Repository: randombit/botan
Pull request #2404 (2020-09-25 09:24:35)

Support suffixes on version numbers
This extends the versioning from just an integer trip to also support a suffix of the form -{alpha,beta,rc}N

Also fix a problem with reproducible releases caused by Python tarfile switching its default format from GNU to PAX. Use PAX for the releases that were (unintentionally) released as PAX and GNU for everything else.

lgtm-com[bot]: This pull request **introduces 1 alert** when merging f8bac02cbb07324e2fd96f04ecc489d07c85f4d2 into d317d8f00baef362609f80b4bad3e63582acfe83 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-3bae2e183a4b1955d0db5d091d493176496d1660)

**new alerts:**

* 1 for Duplication in regular expression character class

Patch:
diff --git a/configure.py b/configure.py
index 9c7d517e9..a8ab62536 100755
--- a/configure.py
+++ b/configure.py
@@ -90,6 +90,13 @@ class Version(object):
         if not Version.data:
             root_dir = os.path.dirname(os.path.realpath(__file__))
             Version.data = parse_version_file(os.path.join(root_dir, 'src/build-data/version.txt'))
+
+            suffix = Version.data["release_suffix"]
+            if suffix != "":
+                suffix_re = re.compile('-(alpha|beta|rc)[0-9]+')
+
+                if not suffix_re.match(suffix):
+                    raise Exception("Unexpected version suffix '%s'" % (suffix))
         return Version.data
 
     @staticmethod
@@ -104,6 +111,10 @@ class Version(object):
     def patch():
         return Version.get_data()["release_patch"]
 
+    @staticmethod
+    def suffix():
+        return Version.get_data()["release_suffix"]
+
     @staticmethod
     def packed():
          # Used on macOS for dylib versioning
@@ -123,7 +134,7 @@ class Version(object):
 
     @staticmethod
     def as_string():
-        return '%d.%d.%d' % (Version.major(), Version.minor(), Version.patch())
+        return '%d.%d.%d%s' % (Version.major(), Version.minor(), Version.patch(), Version.suffix())
 
     @staticmethod
     def vc_rev():
@@ -1981,6 +1992,7 @@ def create_template_vars(source_paths, build_paths, options, modules, cc, arch,
         'version_major':  Version.major(),
         'version_minor':  Version.minor(),
         'version_patch':  Version.patch(),
+        'version_suffix': Version.suffix(),
         'version_vc_rev': 'unknown' if options.no_store_vc_rev else Version.vc_rev(),
         'abi_rev':        Version.so_rev(),
 
diff --git a/src/build-data/buildh.in b/src/build-data/buildh.in
index c33e80acf..cede20ace 100644
--- a/src/build-data/buildh.in
+++ b/src/build-data/buildh.in
@@ -2,7 +2,9 @@
 #define BOTAN_BUILD_CONFIG_H_
 
 /*
-* This file was automatically generated running
+* Build configuration for Botan %{version}
+*
+* Automatically generated from
 * '%{command_line}'
 *
 * Target
@@ -16,6 +18,11 @@
 #define BOTAN_VERSION_PATCH %{version_patch}
 #define BOTAN_VERSION_DATESTAMP %{version_datestamp}
 
+%{if version_suffix}
+#define BOTAN_VERSION_SUFFIX %{version_suffix}
+#define BOTAN_VERSION_SUFFIX_STR "%{version_suffix}"
+%{endif}
+
 #define BOTAN_VERSION_RELEASE_TYPE "%{release_type}"
 
 #define BOTAN_VERSION_VC_REVISION "%{version_vc_rev}"
diff --git a/src/build-data/version.txt b/src/build-data/version.txt
index 99ef56cc2..a823da188 100644
--- a/src/build-data/version.txt
+++ b/src/build-data/version.txt
@@ -2,6 +2,7 @@
 release_major = 2
 release_minor = 16
 release_patch = 0
+release_suffix = ''
 release_so_abi_rev = 15
 
 # These are set by the distribution script
diff --git a/src/lib/utils/version.cpp b/src/lib/utils/version.cpp
index ccf83bf6c..aa82d7cc9 100644
--- a/src/lib/utils/version.cpp
+++ b/src/lib/utils/version.cpp
@@ -23,7 +23,11 @@ const char* short_version_cstr()
    {
    return STR(BOTAN_VERSION_MAJOR) "."
           STR(BOTAN_VERSION_MINOR) "."
-          STR(BOTAN_VERSION_PATCH);
+          STR(BOTAN_VERSION_PATCH)
+#if defined(BOTAN_VERSION_SUFFIX)
+          STR(BOTAN_VERSION_SUFFIX)
+#endif
+      ;
    }
 
 const char* version_cstr()
@@ -36,7 +40,11 @@ const char* version_cstr()
 
    return "Botan " STR(BOTAN_VERSION_MAJOR) "."
                    STR(BOTAN_VERSION_MINOR) "."
-                   STR(BOTAN_VERSION_PATCH) " ("
+                   STR(BOTAN_VERSION_PATCH)
+#if defined(BOTAN_VERSION_SUFFIX)
+                   STR(BOTAN_VERSION_SUFFIX)
+#endif
+                   " ("
 #if defined(BOTAN_UNSAFE_FUZZER_MODE)
                    "UNSAFE FUZZER MODE BUILD "
 #endif
diff --git a/src/scripts/dist.py b/src/scripts/dist.py
index a43b2a468..ce072ec10 100755
--- a/src/scripts/dist.py
+++ b/src/scripts/dist.py
@@ -164,32 +164,36 @@ def rewrite_version_file(version_file, target_version, snapshot_branch, rev_id,
     if snapshot_branch:
         assert target_version == snapshot_branch
 
-    version_file_name = os.path.basename(version_file)
-
     contents = open(version_file).readlines()
 
     version_re = re.compile('release_(major|minor|patch) = ([0-9]+)')
+    version_suffix_re = re.compile('release_suffix = \'(-(alpha|beta|rc)[0-9]+)\'')
+
+    def content_rewriter(target_version):
+        version_info = {}
+
+        release_type = 'release'
+
+        # Not included in old version files so set a default
+        version_info["suffix"] = ""
 
-    def content_rewriter():
         for line in contents:
 
             if not snapshot_branch:
-                # Verify the version set in the source matches the tag
                 match = version_re.match(line)
                 if match:
-                    name_to_idx = {
-                        'major': 0,
-                        'minor': 1,
-                        'patch': 2
-                    }
-                    version_parts = target_version.split('.')
-                    assert len(version_parts) == 3
-                    in_tag = int(version_parts[name_to_idx[match.group(1)]])
-                    in_file = int(match.group(2))
-
-                    if in_tag != in_file:
-                        raise Exception('Version number part "%s" in %s does not match tag %s' %
-                                        (match.group(1), version_file_name, target_version))
+                    version_info[match.group(1)] = int(match.group(2))
+
+                match = version_suffix_re.match(line)
+                if match:
+                    suffix = match.group(1)
+                    version_info['suffix'] = suffix
+                    if suffix.find('alpha') >= 0:
+                        release_type = 'alpha'
+                    elif suffix.find('beta') >= 0:
+                        release_type = 'beta'
+                    elif suffix.find('rc') >= 0:
+                        release_type = 'release candidate'
 
             if line == 'release_vc_rev = None\n':
                 yield 'release_vc_rev = \'git:%s\'\n' % (rev_id)
@@ -199,14 +203,29 @@ def rewrite_version_file(version_file, target_version, snapshot_branch, rev_id,
                 if target_version == snapshot_branch:
                     yield "release_type = 'snapshot:%s'\n" % (snapshot_branch)
                 else:
-                    yield "release_type = 'release'\n"
+                    yield "release_type = '%s'\n" % (release_type)
             else:
                 yield line
 
-    open(version_file, 'w').write(''.join(list(content_rewriter())))
+        if not snapshot_branch:
+            for req_var in ["major", "minor", "patch", "suffix"]:
+                if req_var not in version_info.keys():
+                    raise Exception('Missing version field for %s in version file' % (req_var))
+
+            marked_version = "%d.%d.%d%s" % (version_info["major"],
+                                             version_info["minor"],
+                                             version_info["patch"],
+                                             version_info["suffix"])
 
-def write_archive(output_basename, archive_type, rel_epoch, all_files, hash_file):
+            if marked_version != target_version:
+                raise Exception('Release version file %s does not match tagged version %s' % (
+                    marked_version, target_version))
 
+    new_contents = ''.join(list(content_rewriter(target_version)))
+    open(version_file, 'w').write(new_contents)
+
+def write_archive(version, output_basename, archive_type, rel_epoch, all_files, hash_file):
+    # pylint: disable=too-many-locals
     def archive_suffix(archive_type):
         if archive_type == 'tgz':
             return 'tgz'
@@ -240,14 +259,21 @@ def write_archive(output_basename, archive_type, rel_epoch, all_files, hash_file
     # gzip format embeds the original filename, tarfile.py does the wrong
     # thing unless the output name ends in .gz. So pass an explicit
     # fileobj in that case, and supply a name in the form tarfile expects.
-    if archive_type == 'tgz':
-        archive = tarfile.open(output_basename + '.tar.gz',
-                               write_mode(archive_type),
-                               fileobj=open(output_archive, 'wb'))
-    else:
-        archive = tarfile.open(output_basename + '.tar',
-                               write_mode(archive_type),
-                               fileobj=open(output_archive, 'wb'))
+    archive_suffix = '.tar.gz' if archive_type == 'tgz' else '.tar'
+
+    def archive_format(version):
+        # A change in Python meant that 2.14 and 2.15 were released with a
+        # tarfile using POSIX pax format (the new default for tarfile module)
+        # instead of the previously used GNU format.
+        if version in ['2.14.0', '2.15.0']:
+            return tarfile.PAX_FORMAT
+        else:
+            return tarfile.GNU_FORMAT
+
+    archive = tarfile.open(output_basename + archive_suffix,
+                           write_mode(archive_type),
+                           format=archive_format(version),
+                           fileobj=open(output_archive, 'wb'))
 
     for f in all_files:
         tarinfo = archive.gettarinfo(f)
@@ -329,10 +355,6 @@ def main(args=None):
     elif len(args) == 1:
         try:
             logging.info('Creating release for version %s' % (target_version))
-
-            (major, minor, patch) = [int(x) for x in target_version.split('.')]
-
-            assert target_version == '%d.%d.%d' % (major, minor, patch)
         except ValueError as e:
             logging.error('Invalid version number %s' % (target_version))
 
@@ -404,7 +426,12 @@ def main(args=None):
         hash_file = open(options.write_hash_file, 'w')
 
     for archive_type in archives:
-        output_files.append(write_archive(output_basename, archive_type, rel_epoch, all_files, hash_file))
+        output_files.append(write_archive(target_version,
+                                          output_basename,
+                                          archive_type,
+                                          rel_epoch,
+                                          all_files,
+                                          hash_file))
 
     if hash_file is not None:
         hash_file.close()


Test patch:
diff --git a/src/tests/test_utils.cpp b/src/tests/test_utils.cpp
index 19350e2d3..efcffc45a 100644
--- a/src/tests/test_utils.cpp
+++ b/src/tests/test_utils.cpp
@@ -376,11 +376,15 @@ class Version_Tests final : public Test
          std::string sversion_str = Botan::short_version_string();
          result.test_eq("Same short version string", sversion_str, std::string(sversion_cstr));
 
-         const std::string expected_sversion =
+         std::string expected_sversion =
             std::to_string(BOTAN_VERSION_MAJOR) + "." +
             std::to_string(BOTAN_VERSION_MINOR) + "." +
             std::to_string(BOTAN_VERSION_PATCH);
 
+#if defined(BOTAN_VERSION_SUFFIX)
+         expected_sversion += BOTAN_VERSION_SUFFIX_STR;
+#endif
+
          result.test_eq("Short version string has expected format",
                         sversion_str, expected_sversion);