/ make-tag.py
make-tag.py
  1  #!/usr/bin/env python3
  2  '''
  3  Make a new release tag, performing a few checks.
  4  
  5  Usage: make-tag.py <tag>
  6  '''
  7  import os
  8  import subprocess
  9  import re
 10  import sys
 11  import collections
 12  
 13  import treehash512
 14  
 15  GIT = os.getenv("GIT", "git")
 16  
 17  # Full version specification
 18  VersionSpec = collections.namedtuple('VersionSpec', ['major', 'minor', 'build', 'rc'])
 19  
 20  def version_name(spec):
 21      '''
 22      Short version name for comparison.
 23      '''
 24      if not spec.build:
 25          version = f"{spec.major}.{spec.minor}"
 26      else:
 27          version = f"{spec.major}.{spec.minor}.{spec.build}"
 28      if spec.rc:
 29          version += f"rc{spec.rc}"
 30      return version
 31  
 32  def parse_tag(tag):
 33      '''
 34      Parse a version tag. Valid version tags are
 35  
 36      - v1.2
 37      - v1.2.3
 38      - v1.2rc3
 39      - v1.2.3rc4
 40      '''
 41      m = re.match(r"^v([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:rc([0-9])+)?$", tag)
 42  
 43      if m is None:
 44          print(f"Invalid tag {tag}", file=sys.stderr)
 45          sys.exit(1)
 46  
 47      major = m.group(1)
 48      minor = m.group(2)
 49      build = m.group(3)
 50      rc = m.group(4)
 51  
 52      # Check for x.y.z.0 or x.y.zrc0
 53      if build == '0' or rc == '0':
 54          print('rc or build cannot be specified as 0 (leave them out instead)', file=sys.stderr)
 55          sys.exit(1)
 56  
 57      # Implicitly, treat no rc as rc0 and no build as build 0
 58      if build is None:
 59          build = 0
 60      if rc is None:
 61          rc = 0
 62  
 63      return VersionSpec(int(major), int(minor), int(build), int(rc))
 64  
 65  def check_buildsystem(spec):
 66      '''
 67      Parse configure.ac or CMakeLists.txt and return
 68      (major, minor, build, rc)
 69      '''
 70      info = {}
 71      filename = 'configure.ac'
 72      if os.path.exists(filename):
 73          pattern = r"define\(_CLIENT_VERSION_([A-Z_]+), ([0-9a-z]+)\)"
 74      else:
 75          filename = 'CMakeLists.txt'
 76          if not os.path.exists(filename):
 77              print("No buildsystem (configure.ac or CMakeLists.txt) found", file=sys.stderr)
 78              sys.exit(1)
 79          pattern = r'set\(CLIENT_VERSION_([A-Z_]+)\s+"?([0-9a-z]+)"?\)'
 80  
 81      with open(filename) as f:
 82          for line in f:
 83              m = re.match(pattern, line)
 84              if m:
 85                  info[m.group(1)] = m.group(2)
 86      # check if IS_RELEASE is set
 87      if info["IS_RELEASE"] != "true":
 88          print(f'{filename}: IS_RELEASE is not set to true', file=sys.stderr)
 89          sys.exit(1)
 90  
 91      cfg_spec = VersionSpec(
 92              int(info['MAJOR']),
 93              int(info['MINOR']),
 94              int(info['BUILD']),
 95              int(info['RC']),
 96          )
 97  
 98      if cfg_spec != spec:
 99          print(f"{filename}: Version from tag {version_name(spec)} doesn't match specified version {version_name(cfg_spec)}", file=sys.stderr)
100          sys.exit(1)
101  
102  def main():
103      try:
104          tag = sys.argv[1]
105      except IndexError:
106          print("Usage: make-tag.py <tag>, e.g. v29.0 or v29.1rc3", file=sys.stderr)
107          sys.exit(1)
108  
109      spec = parse_tag(tag)
110  
111      # Check that the script is called from repo root
112      if not os.path.exists('.git'):
113          print('Execute this script at the root of the repository', file=sys.stderr)
114          sys.exit(1)
115  
116      # Check if working directory clean
117      if subprocess.call([GIT, 'diff-index', '--quiet', 'HEAD']):
118          print('Git working directory is not clean. Commit changes first.', file=sys.stderr)
119          sys.exit(1)
120  
121      # Check version components against configure.ac in git tree
122      check_buildsystem(spec)
123  
124      # Generate base message
125      if not spec.build:
126          version = f"{spec.major}.{spec.minor}"
127      else:
128          version = f"{spec.major}.{spec.minor}.{spec.build}"
129      if spec.rc:
130          version += f" release candidate {spec.rc}"
131      else:
132          version += " final"
133      msg = 'Bitcoin Core ' + version + '\n'
134  
135      # Add treehash header
136      msg += "\n"
137      msg += 'Tree-SHA512: ' + treehash512.tree_sha512sum() + '\n'
138  
139      # Finally, make the tag
140      print(msg)
141      return subprocess.call([GIT, "tag", "-s", tag, "-m", msg])
142  
143  if __name__ == '__main__':
144      sys.exit(main())