From 6f180cc6c40d32180cecbbcc32b306f36096d80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 15 Aug 2018 18:24:43 +0200 Subject: [PATCH] Scripted version bump + release branch creation --- VERSIONING.md | 36 ++ .../third_party/packaging/__init__.py | 3 + .../third_party/packaging/_structures.py | 68 +++ .../third_party/packaging/version.py | 393 ++++++++++++++++++ benchmark/space/draw.py | 2 +- benchmark/version.py | 106 ++++- bundle/bundle-linux.sh | 7 +- create_version_branch.sh | 74 ++++ 8 files changed, 679 insertions(+), 10 deletions(-) create mode 100644 VERSIONING.md create mode 100644 benchmark/foundation/third_party/packaging/__init__.py create mode 100644 benchmark/foundation/third_party/packaging/_structures.py create mode 100644 benchmark/foundation/third_party/packaging/version.py create mode 100755 create_version_branch.sh diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..7101193 --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,36 @@ +# Blender Benchmark Client versioning guideline + +The version of the Benchmark Client is stored in `benchmark/version.py`. +The version number MUST adhere to [PEP 440](https://www.python.org/dev/peps/pep-0440/). + +## TL;DR + +To build a release branch for version `1.3`, run: + + ./create_version_branch.sh 1.3 + +This will create a branch `release-1.3`, which will be checked out in your +working directory once it's done. Furthermore, it'll bump the version in the +`master` branch to `1.4.dev0`. If you release a beta release, it'll bump the +beta number instead of the minor number. + + +## The long text + +Since we make it as easy as possible to submit benchmark data, it is +conceivable that test data is submitted accidentally during development. +To reduce the impact of such a mistake, the version should end in `.devN` +(where `N` is a nonnegative integer) for all in-development versions. +Only versions that are actually released should drop this suffix. A typical +development flow for release `1.3` would thus be: + +- Set version to `1.3.dev0` and commit in Git. +- Do more development until 1.3 is ready to be released. +- Create a branch `release-1.3` for the release. +- On `master` branch: set version to `1.4.dev0` and commit. +- On release branch: set version to `1.3` and commit. +- Tag this commit as `v1.3`. +- Build the release files based on the `v1.3` tag or `release-1.3` branch. +- Use the release branch for fixes etc. + +This way the `master` branch always has development versions. diff --git a/benchmark/foundation/third_party/packaging/__init__.py b/benchmark/foundation/third_party/packaging/__init__.py new file mode 100644 index 0000000..4ac0ea8 --- /dev/null +++ b/benchmark/foundation/third_party/packaging/__init__.py @@ -0,0 +1,3 @@ +# These files were copied from the pkg_resource Python package, and altered +# to suit our needs (renamed the `Version._version` attribute to +# `Version.version`). diff --git a/benchmark/foundation/third_party/packaging/_structures.py b/benchmark/foundation/third_party/packaging/_structures.py new file mode 100644 index 0000000..ccc2786 --- /dev/null +++ b/benchmark/foundation/third_party/packaging/_structures.py @@ -0,0 +1,68 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +from __future__ import absolute_import, division, print_function + + +class Infinity(object): + + def __repr__(self): + return "Infinity" + + def __hash__(self): + return hash(repr(self)) + + def __lt__(self, other): + return False + + def __le__(self, other): + return False + + def __eq__(self, other): + return isinstance(other, self.__class__) + + def __ne__(self, other): + return not isinstance(other, self.__class__) + + def __gt__(self, other): + return True + + def __ge__(self, other): + return True + + def __neg__(self): + return NegativeInfinity + +Infinity = Infinity() + + +class NegativeInfinity(object): + + def __repr__(self): + return "-Infinity" + + def __hash__(self): + return hash(repr(self)) + + def __lt__(self, other): + return True + + def __le__(self, other): + return True + + def __eq__(self, other): + return isinstance(other, self.__class__) + + def __ne__(self, other): + return not isinstance(other, self.__class__) + + def __gt__(self, other): + return False + + def __ge__(self, other): + return False + + def __neg__(self): + return Infinity + +NegativeInfinity = NegativeInfinity() diff --git a/benchmark/foundation/third_party/packaging/version.py b/benchmark/foundation/third_party/packaging/version.py new file mode 100644 index 0000000..e90c354 --- /dev/null +++ b/benchmark/foundation/third_party/packaging/version.py @@ -0,0 +1,393 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +from __future__ import absolute_import, division, print_function + +import collections +import itertools +import re + +from ._structures import Infinity + + +__all__ = [ + "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN" +] + + +_Version = collections.namedtuple( + "_Version", + ["epoch", "release", "dev", "pre", "post", "local"], +) + + +def parse(version): + """ + Parse the given version string and return either a :class:`Version` object + or a :class:`LegacyVersion` object depending on if the given version is + a valid PEP 440 version or a legacy version. + """ + try: + return Version(version) + except InvalidVersion: + return LegacyVersion(version) + + +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + +class _BaseVersion(object): + + def __hash__(self): + return hash(self._key) + + def __lt__(self, other): + return self._compare(other, lambda s, o: s < o) + + def __le__(self, other): + return self._compare(other, lambda s, o: s <= o) + + def __eq__(self, other): + return self._compare(other, lambda s, o: s == o) + + def __ge__(self, other): + return self._compare(other, lambda s, o: s >= o) + + def __gt__(self, other): + return self._compare(other, lambda s, o: s > o) + + def __ne__(self, other): + return self._compare(other, lambda s, o: s != o) + + def _compare(self, other, method): + if not isinstance(other, _BaseVersion): + return NotImplemented + + return method(self._key, other._key) + + +class LegacyVersion(_BaseVersion): + + def __init__(self, version): + self._version = str(version) + self._key = _legacy_cmpkey(self._version) + + def __str__(self): + return self._version + + def __repr__(self): + return "".format(repr(str(self))) + + @property + def public(self): + return self._version + + @property + def base_version(self): + return self._version + + @property + def local(self): + return None + + @property + def is_prerelease(self): + return False + + @property + def is_postrelease(self): + return False + + +_legacy_version_component_re = re.compile( + r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE, +) + +_legacy_version_replacement_map = { + "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", +} + + +def _parse_version_parts(s): + for part in _legacy_version_component_re.split(s): + part = _legacy_version_replacement_map.get(part, part) + + if not part or part == ".": + continue + + if part[:1] in "0123456789": + # pad for numeric comparison + yield part.zfill(8) + else: + yield "*" + part + + # ensure that alpha/beta/candidate are before final + yield "*final" + + +def _legacy_cmpkey(version): + # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch + # greater than or equal to 0. This will effectively put the LegacyVersion, + # which uses the defacto standard originally implemented by setuptools, + # as before all PEP 440 versions. + epoch = -1 + + # This scheme is taken from pkg_resources.parse_version setuptools prior to + # it's adoption of the packaging library. + parts = [] + for part in _parse_version_parts(version.lower()): + if part.startswith("*"): + # remove "-" before a prerelease tag + if part < "*final": + while parts and parts[-1] == "*final-": + parts.pop() + + # remove trailing zeros from each series of numeric parts + while parts and parts[-1] == "00000000": + parts.pop() + + parts.append(part) + parts = tuple(parts) + + return epoch, parts + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+
+class Version(_BaseVersion):
+
+    _regex = re.compile(
+        r"^\s*" + VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    def __init__(self, version):
+        # Validate the version and parse it into pieces
+        match = self._regex.search(version)
+        if not match:
+            raise InvalidVersion("Invalid version: '{0}'".format(version))
+
+        # Store the parsed out pieces of the version
+        self.version = _Version(
+            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+            release=tuple(int(i) for i in match.group("release").split(".")),
+            pre=_parse_letter_version(
+                match.group("pre_l"),
+                match.group("pre_n"),
+            ),
+            post=_parse_letter_version(
+                match.group("post_l"),
+                match.group("post_n1") or match.group("post_n2"),
+            ),
+            dev=_parse_letter_version(
+                match.group("dev_l"),
+                match.group("dev_n"),
+            ),
+            local=_parse_local_version(match.group("local")),
+        )
+
+        # Generate a key which will be used for sorting
+        self._key = _cmpkey(
+            self.version.epoch,
+            self.version.release,
+            self.version.pre,
+            self.version.post,
+            self.version.dev,
+            self.version.local,
+        )
+
+    def __repr__(self):
+        return "".format(repr(str(self)))
+
+    def __str__(self):
+        parts = []
+
+        # Epoch
+        if self.version.epoch != 0:
+            parts.append("{0}!".format(self.version.epoch))
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.version.release))
+
+        # Pre-release
+        if self.version.pre is not None:
+            parts.append("".join(str(x) for x in self.version.pre))
+
+        # Post-release
+        if self.version.post is not None:
+            parts.append(".post{0}".format(self.version.post[1]))
+
+        # Development release
+        if self.version.dev is not None:
+            parts.append(".dev{0}".format(self.version.dev[1]))
+
+        # Local version segment
+        if self.version.local is not None:
+            parts.append(
+                "+{0}".format(".".join(str(x) for x in self.version.local))
+            )
+
+        return "".join(parts)
+
+    @property
+    def public(self):
+        return str(self).split("+", 1)[0]
+
+    @property
+    def base_version(self):
+        parts = []
+
+        # Epoch
+        if self.version.epoch != 0:
+            parts.append("{0}!".format(self.version.epoch))
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.version.release))
+
+        return "".join(parts)
+
+    @property
+    def local(self):
+        version_string = str(self)
+        if "+" in version_string:
+            return version_string.split("+", 1)[1]
+
+    @property
+    def is_prerelease(self):
+        return bool(self.version.dev or self.version.pre)
+
+    @property
+    def is_postrelease(self):
+        return bool(self.version.post)
+
+
+def _parse_letter_version(letter, number):
+    if letter:
+        # We consider there to be an implicit 0 in a pre-release if there is
+        # not a numeral associated with it.
+        if number is None:
+            number = 0
+
+        # We normalize any letters to their lower case form
+        letter = letter.lower()
+
+        # We consider some words to be alternate spellings of other words and
+        # in those cases we want to normalize the spellings to our preferred
+        # spelling.
+        if letter == "alpha":
+            letter = "a"
+        elif letter == "beta":
+            letter = "b"
+        elif letter in ["c", "pre", "preview"]:
+            letter = "rc"
+        elif letter in ["rev", "r"]:
+            letter = "post"
+
+        return letter, int(number)
+    if not letter and number:
+        # We assume if we are given a number, but we are not given a letter
+        # then this is using the implicit post release syntax (e.g. 1.0-1)
+        letter = "post"
+
+        return letter, int(number)
+
+
+_local_version_seperators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local):
+    """
+    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+    """
+    if local is not None:
+        return tuple(
+            part.lower() if not part.isdigit() else int(part)
+            for part in _local_version_seperators.split(local)
+        )
+
+
+def _cmpkey(epoch, release, pre, post, dev, local):
+    # When we compare a release version, we want to compare it with all of the
+    # trailing zeros removed. So we'll use a reverse the list, drop all the now
+    # leading zeros until we come to something non zero, then take the rest
+    # re-reverse it back into the correct order and make it a tuple and use
+    # that for our sorting key.
+    release = tuple(
+        reversed(list(
+            itertools.dropwhile(
+                lambda x: x == 0,
+                reversed(release),
+            )
+        ))
+    )
+
+    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+    # We'll do this by abusing the pre segment, but we _only_ want to do this
+    # if there is not a pre or a post segment. If we have one of those then
+    # the normal sorting rules will handle this case correctly.
+    if pre is None and post is None and dev is not None:
+        pre = -Infinity
+    # Versions without a pre-release (except as noted above) should sort after
+    # those with one.
+    elif pre is None:
+        pre = Infinity
+
+    # Versions without a post segment should sort before those with one.
+    if post is None:
+        post = -Infinity
+
+    # Versions without a development segment should sort after those with one.
+    if dev is None:
+        dev = Infinity
+
+    if local is None:
+        # Versions without a local segment should sort before those with one.
+        local = -Infinity
+    else:
+        # Versions with a local segment need that segment parsed to implement
+        # the sorting rules in PEP440.
+        # - Alpha numeric segments sort before numeric segments
+        # - Alpha numeric segments sort lexicographically
+        # - Numeric segments sort numerically
+        # - Shorter versions sort before longer versions when the prefixes
+        #   match exactly
+        local = tuple(
+            (i, "") if isinstance(i, int) else (-Infinity, i)
+            for i in local
+        )
+
+    return epoch, release, pre, post, dev, local
diff --git a/benchmark/space/draw.py b/benchmark/space/draw.py
index 81120b9..0093562 100644
--- a/benchmark/space/draw.py
+++ b/benchmark/space/draw.py
@@ -140,7 +140,7 @@ def _draw_introduction(image_y, ui_scale, window_width, window_height):
     x = 0.5 * window_width
     y = 0.70 * window_height
     blf.size(font_id, int(32 * ui_scale), 72)
-    draw_text_center(f"Blender Benchmark {version.version_human}", x, y, shadow=True)
+    draw_text_center(f"Blender Benchmark {version.formatted()}", x, y, shadow=True)
 
     y -= 32 * ui_scale
     blf.size(font_id, int(12 * ui_scale), 72)
diff --git a/benchmark/version.py b/benchmark/version.py
index e6a4292..09b5c19 100644
--- a/benchmark/version.py
+++ b/benchmark/version.py
@@ -1,7 +1,103 @@
-"""Version info for the Benchmark Client."""
+"""Version info for the Benchmark Client.
 
-# Included in error reports.
-version = '1.0-beta'
+Run with `python3 -m benchmark.version` to print the version number.
+"""
 
-# Shown on the splash.
-version_human = '1.0 Beta'
+import functools
+
+# This version number MUST adhere to PEP 440.
+# https://www.python.org/dev/peps/pep-0440/
+#
+# It is sent to the MyData server in the 'User Agent' HTTP header, and
+# it's also included in error reports.
+version = '1.1.dev0'
+
+
+@functools.lru_cache(maxsize=1)
+def formatted(the_version=version) -> str:
+    """Format the version for showing on the splash screen.
+
+    >>> formatted('1.0')
+    '1.0'
+    >>> formatted('1.0b2')
+    '1.0 Beta 2'
+    >>> formatted('1.0rc3')
+    '1.0 Release Candidate 3'
+    >>> formatted('1.0b2.dev0')
+    '1.0 Beta 2 (development)'
+    >>> formatted('1.0.dev0')
+    '1.0 (development)'
+    >>> formatted('1.0a0.dev0+e0e752c')
+    '1.0 Alpha (development e0e752c)'
+    """
+
+    from benchmark.foundation.third_party.packaging.version import parse
+
+    parsed_version = parse(the_version)
+    inner_version = parsed_version.version
+
+    parts = [parsed_version.base_version]
+
+    if inner_version.pre:
+        a_b_rc, num = inner_version.pre
+        name = {'a': 'Alpha',
+                'b': 'Beta',
+                'rc': 'Release Candidate'}[a_b_rc]
+        if num:
+            parts.append(f'{name} {num}')
+        else:
+            parts.append(name)
+
+    local_ver = parsed_version.local
+    if local_ver:
+        parts.append(f'(development {local_ver})')
+    elif inner_version.dev:
+        parts.append(f'(development)')
+
+    return ' '.join(parts)
+
+
+def next_dev_version(the_version=version) -> str:
+    """Returns the version string of the next development version.
+
+    If an alpha/beta/RC version was given, that number is increased.
+    Otherwise the minor revision is increased.
+
+    >>> next_dev_version('1')
+    '1.1.dev0'
+    >>> next_dev_version('1.0')
+    '1.1.dev0'
+    >>> next_dev_version('1.0b2')
+    '1.0b3.dev0'
+    >>> next_dev_version('1.0rc3')
+    '1.0rc4.dev0'
+    >>> next_dev_version('2.51.dev0')
+    '2.52.dev0'
+    >>> next_dev_version('2.51.dev0+localstuff')
+    '2.52.dev0'
+    """
+
+    from benchmark.foundation.third_party.packaging.version import parse
+
+    parsed_version = parse(the_version)
+    inner_version = parsed_version.version
+
+    # Increase the alpha/beta/RC number
+    if inner_version.pre:
+        a_b_rc, num = inner_version.pre
+        return f'{parsed_version.base_version}{a_b_rc}{num+1}.dev0'
+
+    # Increase the minor release
+    major = inner_version.release[0]
+    if len(inner_version.release) == 1:
+        return f'{major}.1.dev0'
+
+    minor = inner_version.release[1] + 1
+    return f'{major}.{minor}.dev0'
+
+
+if __name__ == '__main__':
+    import doctest
+
+    doctest.testmod()
+    print(version)
diff --git a/bundle/bundle-linux.sh b/bundle/bundle-linux.sh
index 224fb7e..a05ef93 100755
--- a/bundle/bundle-linux.sh
+++ b/bundle/bundle-linux.sh
@@ -39,7 +39,8 @@ else
 fi
 
 SCRIPTPATH=`dirname $SCRIPT`
-BENCHMARK_VERSION="1.0beta"
+ROOTPATH=`dirname $SCRIPTPATH`
+BENCHMARK_VERSION=`PYTHONPATH=${ROOTPATH} python3 -m benchmark.version`
 
 CONFIG="${SCRIPTPATH}/config"
 
@@ -260,9 +261,7 @@ GIT_C="${GIT} -C ${SOURCE_DIRECTORY}"
 
 if [ ! -d "${SOURCE_DIRECTORY}" ]; then
   echo "Checking out Blender.git..."
-  ${GIT} clone "${GIT_BLENDER_REPOSITORY}" "${SOURCE_DIRECTORY}"
-  echo "Switching to benchmark branch..."
-  ${GIT_C} checkout ${GIT_BENCHMARK_BRANCH}
+  ${GIT} clone --branch ${GIT_BENCHMARK_BRANCH} "${GIT_BLENDER_REPOSITORY}" "${SOURCE_DIRECTORY}"
   echo "Initializing submodules..."
   ${GIT_C} submodule update --init --recursive
 else
diff --git a/create_version_branch.sh b/create_version_branch.sh
new file mode 100755
index 0000000..eff05b1
--- /dev/null
+++ b/create_version_branch.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+# See VERSIONING.md for an explanation.
+
+set -e
+RELEASE_VERSION="$1"
+NEXT_DEV_VERSION="$2"
+
+if [ -z "$RELEASE_VERSION" ]; then
+    echo "Usage: $0 release-version [next-dev-version]" >&2
+    exit 1
+fi
+
+if [ "${KERNEL_NAME}" == "Darwin" ]; then
+  SCRIPT=$(realpath $0)
+else
+  SCRIPT=$(readlink -f $0)
+fi
+SCRIPTPATH=`dirname $SCRIPT`
+CURRENT_VERSION=`PYTHONPATH=${SCRIPTPATH} python3 -m benchmark.version`
+
+if [ -z "$NEXT_DEV_VERSION" ]; then
+    NEXT_DEV_VERSION=`PYTHONPATH=${SCRIPTPATH} python3 <&2
+        exit 2
+    fi
+fi
+
+RELEASE_BRANCH="release-$RELEASE_VERSION"
+
+echo "Current version    : $CURRENT_VERSION"
+echo "To be released     : $RELEASE_VERSION in branch $RELEASE_BRANCH"
+echo "Next master version: $NEXT_DEV_VERSION"
+
+GIT=`which git`
+if [ -z "${GIT}" ]; then
+  echo "ERROR: Git is not found, can not continue."
+  exit 1
+fi
+
+GIT_C="${GIT} -C ${SCRIPTPATH}"
+if [ $(${GIT_C} rev-parse --abbrev-ref HEAD) != "master" ]; then
+    echo "You are NOT on the master branch, refusing to run." >&2
+    exit 3
+fi
+
+echo
+echo "Press [ENTER] to create version commits and the release branch."
+if [ -z "$2" ]; then
+    echo "If you don't like the auto-generated next dev version, pass it on the CLI."
+fi
+read dummy
+
+echo "Creating branch $RELEASE_BRANCH"
+${GIT_C} checkout master -b ${RELEASE_BRANCH}
+sed -i'' "s/^version = '.*'/version = '$RELEASE_VERSION'/" benchmark/version.py
+${GIT_C} commit -m "Bumped version to $RELEASE_VERSION" benchmark/version.py
+
+echo "Bumping version to $NEXT_DEV_VERSION in master branch"
+${GIT_C} checkout master
+sed -i'' "s/^version = '.*'/version = '$NEXT_DEV_VERSION'/" benchmark/version.py
+${GIT_C} commit -m "Bumped version to $NEXT_DEV_VERSION" benchmark/version.py
+
+echo "Checking out branch $RELEASE_BRANCH for bundling"
+${GIT_C} checkout ${RELEASE_BRANCH}
+
+echo "Done. Please investigate and push both master and $RELEASE_BRANCH branches:"
+echo
+echo "git push origin $RELEASE_BRANCH"
+echo "git co master && git push origin master"