/ 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())