forked from hush/hush3
Duke Leto
5 years ago
1 changed files with 0 additions and 668 deletions
@ -1,668 +0,0 @@ |
|||
#! /usr/bin/env python2 |
|||
|
|||
import os |
|||
import re |
|||
import sys |
|||
import logging |
|||
import argparse |
|||
import subprocess |
|||
import traceback |
|||
import unittest |
|||
import random |
|||
from cStringIO import StringIO |
|||
from functools import wraps |
|||
|
|||
|
|||
def main(args=sys.argv[1:]): |
|||
""" |
|||
Perform the final Zcash release process up to the git tag. |
|||
""" |
|||
opts = parse_args(args) |
|||
chdir_to_repo(opts.REPO) |
|||
initialize_logging() |
|||
logging.debug('argv %r', sys.argv) |
|||
|
|||
try: |
|||
main_logged( |
|||
opts.RELEASE_VERSION, |
|||
opts.RELEASE_PREV, |
|||
opts.RELEASE_FROM, |
|||
opts.RELEASE_HEIGHT, |
|||
opts.HOTFIX, |
|||
) |
|||
except SystemExit as e: |
|||
logging.error(str(e)) |
|||
raise SystemExit(1) |
|||
except: |
|||
logging.error(traceback.format_exc()) |
|||
raise SystemExit(2) |
|||
|
|||
|
|||
def parse_args(args): |
|||
p = argparse.ArgumentParser(description=main.__doc__) |
|||
p.add_argument( |
|||
'--repo', |
|||
dest='REPO', |
|||
type=str, |
|||
help='Path to repository root.', |
|||
) |
|||
p.add_argument( |
|||
'--hotfix', |
|||
action='store_true', |
|||
dest='HOTFIX', |
|||
help='Use if this is a hotfix release from a non-master branch.', |
|||
) |
|||
p.add_argument( |
|||
'RELEASE_VERSION', |
|||
type=Version.parse_arg, |
|||
help='The release version: vX.Y.Z-N', |
|||
) |
|||
p.add_argument( |
|||
'RELEASE_PREV', |
|||
type=Version.parse_arg, |
|||
help='The previously released version.', |
|||
) |
|||
p.add_argument( |
|||
'RELEASE_FROM', |
|||
type=Version.parse_arg, |
|||
help='The previously released non-beta non-RC version. May be the same as RELEASE_PREV.', |
|||
) |
|||
p.add_argument( |
|||
'RELEASE_HEIGHT', |
|||
type=int, |
|||
help='A block height approximately occuring on release day.', |
|||
) |
|||
return p.parse_args(args) |
|||
|
|||
|
|||
# Top-level flow: |
|||
def main_logged(release, releaseprev, releasefrom, releaseheight, hotfix): |
|||
verify_tags(releaseprev, releasefrom) |
|||
verify_version(release, releaseprev, hotfix) |
|||
initialize_git(release, hotfix) |
|||
patch_version_in_files(release, releaseprev) |
|||
patch_release_height(releaseheight) |
|||
commit('Versioning changes for {}.'.format(release.novtext)) |
|||
|
|||
build() |
|||
gen_manpages() |
|||
commit('Updated manpages for {}.'.format(release.novtext)) |
|||
|
|||
gen_release_notes(release, releasefrom) |
|||
update_debian_changelog(release) |
|||
commit( |
|||
'Updated release notes and changelog for {}.'.format( |
|||
release.novtext, |
|||
), |
|||
) |
|||
|
|||
|
|||
def phase(message): |
|||
def deco(f): |
|||
@wraps(f) |
|||
def g(*a, **kw): |
|||
logging.info('%s', message) |
|||
return f(*a, **kw) |
|||
return g |
|||
return deco |
|||
|
|||
|
|||
@phase('Checking tags.') |
|||
def verify_tags(releaseprev, releasefrom): |
|||
candidates = [] |
|||
|
|||
# Any tag beginning with a 'v' followed by [1-9] must be a version |
|||
# matching our Version parser. Tags beginning with v0 may exist from |
|||
# upstream and those do not follow our schema and are silently |
|||
# ignored. Any other tag is silently ignored. |
|||
candidatergx = re.compile('^v[1-9].*$') |
|||
|
|||
for tag in sh_out('git', 'tag', '--list').splitlines(): |
|||
if candidatergx.match(tag): |
|||
candidates.append(Version.parse_arg(tag)) |
|||
|
|||
candidates.sort() |
|||
try: |
|||
latest = candidates[-1] |
|||
except IndexError: |
|||
raise SystemExit('No previous releases found by `git tag --list`.') |
|||
|
|||
if releaseprev != latest: |
|||
raise SystemExit( |
|||
'The latest candidate in `git tag --list` is {} not {}' |
|||
.format( |
|||
latest.vtext, |
|||
releaseprev.vtext, |
|||
), |
|||
) |
|||
|
|||
candidates.reverse() |
|||
prev_tags = [] |
|||
for candidate in candidates: |
|||
if releasefrom == candidate: |
|||
break |
|||
else: |
|||
prev_tags.append(candidate) |
|||
else: |
|||
raise SystemExit( |
|||
'{} does not appear in `git tag --list`' |
|||
.format( |
|||
releasefrom.vtext, |
|||
), |
|||
) |
|||
|
|||
for tag in prev_tags: |
|||
if not tag.betarc: |
|||
raise SystemExit( |
|||
'{} appears to be a more recent non-beta non-RC release than {}' |
|||
.format( |
|||
tag.vtext, |
|||
releasefrom.vtext, |
|||
), |
|||
) |
|||
|
|||
|
|||
@phase('Checking version.') |
|||
def verify_version(release, releaseprev, hotfix): |
|||
if not hotfix: |
|||
return |
|||
|
|||
expected = Version( |
|||
releaseprev.major, |
|||
releaseprev.minor, |
|||
releaseprev.patch, |
|||
releaseprev.betarc, |
|||
releaseprev.hotfix + 1 if releaseprev.hotfix else 1, |
|||
) |
|||
if release != expected: |
|||
raise SystemExit( |
|||
"Expected {!r}, given {!r}".format( |
|||
expected, release, |
|||
), |
|||
) |
|||
|
|||
|
|||
@phase('Initializing git.') |
|||
def initialize_git(release, hotfix): |
|||
junk = sh_out('git', 'status', '--porcelain') |
|||
if junk.strip(): |
|||
raise SystemExit('There are uncommitted changes:\n' + junk) |
|||
|
|||
branch = sh_out('git', 'rev-parse', '--abbrev-ref', 'HEAD').strip() |
|||
if hotfix: |
|||
expected = 'hotfix-' + release.vtext |
|||
else: |
|||
expected = 'master' |
|||
if branch != expected: |
|||
raise SystemExit( |
|||
"Expected branch {!r}, found branch {!r}".format( |
|||
expected, branch, |
|||
), |
|||
) |
|||
|
|||
logging.info('Pulling to latest master.') |
|||
sh_log('git', 'pull', '--ff-only') |
|||
|
|||
branch = 'release-' + release.vtext |
|||
logging.info('Creating release branch: %r', branch) |
|||
sh_log('git', 'checkout', '-b', branch) |
|||
return branch |
|||
|
|||
|
|||
@phase('Patching versioning in files.') |
|||
def patch_version_in_files(release, releaseprev): |
|||
patch_README(release, releaseprev) |
|||
patch_clientversion_h(release) |
|||
patch_configure_ac(release) |
|||
patch_gitian_linux_yml(release, releaseprev) |
|||
|
|||
|
|||
@phase('Patching release height for end-of-support halt.') |
|||
def patch_release_height(releaseheight): |
|||
rgx = re.compile( |
|||
r'^(static const int APPROX_RELEASE_HEIGHT = )\d+(;)$', |
|||
) |
|||
with PathPatcher('src/deprecation.h') as (inf, outf): |
|||
for line in inf: |
|||
m = rgx.match(line) |
|||
if m is None: |
|||
outf.write(line) |
|||
else: |
|||
[prefix, suffix] = m.groups() |
|||
outf.write( |
|||
'{}{}{}\n'.format( |
|||
prefix, |
|||
releaseheight, |
|||
suffix, |
|||
), |
|||
) |
|||
|
|||
|
|||
@phase('Building...') |
|||
def build(): |
|||
base_dir = os.getcwd() |
|||
depends_dir = os.path.join(base_dir, 'depends') |
|||
src_dir = os.path.join(base_dir, 'src') |
|||
nproc = sh_out('nproc').strip() |
|||
sh_progress([ |
|||
'Staging boost...', |
|||
'Staging libevent...', |
|||
'Staging zeromq...', |
|||
'Staging libgmp...', |
|||
'Staging libsodium...', |
|||
"Leaving directory '%s'" % depends_dir, |
|||
'config.status: creating libzcashconsensus.pc', |
|||
"Entering directory '%s'" % src_dir, |
|||
'httpserver.cpp', |
|||
'torcontrol.cpp', |
|||
'gtest/test_tautology.cpp', |
|||
'gtest/test_metrics.cpp', |
|||
'test/equihash_tests.cpp', |
|||
'test/util_tests.cpp', |
|||
"Leaving directory '%s'" % src_dir, |
|||
], './zcutil/build.sh', '-j', nproc) |
|||
|
|||
|
|||
@phase('Generating manpages.') |
|||
def gen_manpages(): |
|||
sh_log('./contrib/devtools/gen-manpages.sh') |
|||
|
|||
|
|||
@phase('Generating release notes.') |
|||
def gen_release_notes(release, releasefrom): |
|||
release_notes = [ |
|||
'python', |
|||
'./zcutil/release-notes.py', |
|||
'--version', |
|||
release.novtext, |
|||
'--prev', |
|||
releasefrom.vtext, |
|||
] |
|||
if not release.betarc: |
|||
release_notes.append('--clear') |
|||
sh_log(*release_notes) |
|||
sh_log( |
|||
'git', |
|||
'add', |
|||
'./doc/authors.md', |
|||
'./doc/release-notes/release-notes-{}.md'.format(release.novtext), |
|||
) |
|||
|
|||
|
|||
@phase('Updating debian changelog.') |
|||
def update_debian_changelog(release): |
|||
os.environ['DEBEMAIL'] = 'team@z.cash' |
|||
os.environ['DEBFULLNAME'] = 'Zcash Company' |
|||
sh_log( |
|||
'debchange', |
|||
'--newversion', release.debversion, |
|||
'--distribution', 'stable', |
|||
'--changelog', './contrib/debian/changelog', |
|||
'{} release.'.format(release.novtext), |
|||
) |
|||
|
|||
|
|||
# Helper code: |
|||
def commit(message): |
|||
logging.info('Committing: %r', message) |
|||
fullmsg = 'make-release.py: {}'.format(message) |
|||
sh_log('git', 'commit', '--all', '-m', fullmsg) |
|||
|
|||
|
|||
def chdir_to_repo(repo): |
|||
if repo is None: |
|||
dn = os.path.dirname |
|||
repo = dn(dn(os.path.abspath(sys.argv[0]))) |
|||
os.chdir(repo) |
|||
|
|||
|
|||
def patch_README(release, releaseprev): |
|||
with PathPatcher('README.md') as (inf, outf): |
|||
firstline = inf.readline() |
|||
assert firstline == 'Zcash {}\n'.format(releaseprev.novtext), \ |
|||
repr(firstline) |
|||
|
|||
outf.write('Zcash {}\n'.format(release.novtext)) |
|||
outf.write(inf.read()) |
|||
|
|||
|
|||
def patch_clientversion_h(release): |
|||
_patch_build_defs( |
|||
release, |
|||
'src/clientversion.h', |
|||
(r'^(#define CLIENT_VERSION_(MAJOR|MINOR|REVISION|BUILD|IS_RELEASE))' |
|||
r' \d+()$'), |
|||
) |
|||
|
|||
|
|||
def patch_configure_ac(release): |
|||
_patch_build_defs( |
|||
release, |
|||
'configure.ac', |
|||
(r'^(define\(_CLIENT_VERSION_(MAJOR|MINOR|REVISION|BUILD|IS_RELEASE),)' |
|||
r' \d+(\))$'), |
|||
) |
|||
|
|||
|
|||
def patch_gitian_linux_yml(release, releaseprev): |
|||
path = 'contrib/gitian-descriptors/gitian-linux.yml' |
|||
with PathPatcher(path) as (inf, outf): |
|||
outf.write(inf.readline()) |
|||
|
|||
secondline = inf.readline() |
|||
assert secondline == 'name: "zcash-{}"\n'.format( |
|||
releaseprev.novtext |
|||
), repr(secondline) |
|||
|
|||
outf.write('name: "zcash-{}"\n'.format(release.novtext)) |
|||
outf.write(inf.read()) |
|||
|
|||
|
|||
def _patch_build_defs(release, path, pattern): |
|||
rgx = re.compile(pattern) |
|||
with PathPatcher(path) as (inf, outf): |
|||
for line in inf: |
|||
m = rgx.match(line) |
|||
if m: |
|||
prefix, label, suffix = m.groups() |
|||
repl = { |
|||
'MAJOR': release.major, |
|||
'MINOR': release.minor, |
|||
'REVISION': release.patch, |
|||
'BUILD': release.build, |
|||
'IS_RELEASE': ( |
|||
'false' if release.build < 50 else 'true' |
|||
), |
|||
}[label] |
|||
outf.write('{} {}{}\n'.format(prefix, repl, suffix)) |
|||
else: |
|||
outf.write(line) |
|||
|
|||
|
|||
def initialize_logging(): |
|||
logname = './zcash-make-release.log' |
|||
fmtr = logging.Formatter( |
|||
'%(asctime)s L%(lineno)-4d %(levelname)-5s | %(message)s', |
|||
'%Y-%m-%d %H:%M:%S' |
|||
) |
|||
|
|||
hout = logging.StreamHandler(sys.stdout) |
|||
hout.setLevel(logging.INFO) |
|||
hout.setFormatter(fmtr) |
|||
|
|||
hpath = logging.FileHandler(logname, mode='a') |
|||
hpath.setLevel(logging.DEBUG) |
|||
hpath.setFormatter(fmtr) |
|||
|
|||
root = logging.getLogger() |
|||
root.setLevel(logging.DEBUG) |
|||
root.addHandler(hout) |
|||
root.addHandler(hpath) |
|||
logging.info('zcash make-release.py debug log: %r', logname) |
|||
|
|||
|
|||
def sh_out(*args): |
|||
logging.debug('Run (out): %r', args) |
|||
return subprocess.check_output(args) |
|||
|
|||
|
|||
def sh_log(*args): |
|||
PIPE = subprocess.PIPE |
|||
STDOUT = subprocess.STDOUT |
|||
try: |
|||
p = subprocess.Popen(args, stdout=PIPE, stderr=STDOUT, stdin=None) |
|||
except OSError: |
|||
logging.error('Error launching %r...', args) |
|||
raise |
|||
|
|||
logging.debug('Run (log PID %r): %r', p.pid, args) |
|||
for line in p.stdout: |
|||
logging.debug('> %s', line.rstrip()) |
|||
status = p.wait() |
|||
if status != 0: |
|||
raise SystemExit('Nonzero exit status: {!r}'.format(status)) |
|||
|
|||
|
|||
def sh_progress(markers, *args): |
|||
try: |
|||
import progressbar |
|||
except: |
|||
sh_log(*args) |
|||
return |
|||
|
|||
PIPE = subprocess.PIPE |
|||
STDOUT = subprocess.STDOUT |
|||
try: |
|||
p = subprocess.Popen(args, stdout=PIPE, stderr=STDOUT, stdin=None) |
|||
except OSError: |
|||
logging.error('Error launching %r...', args) |
|||
raise |
|||
|
|||
pbar = progressbar.ProgressBar(max_value=len(markers)) |
|||
marker = 0 |
|||
pbar.update(marker) |
|||
logging.debug('Run (log PID %r): %r', p.pid, args) |
|||
for line in p.stdout: |
|||
logging.debug('> %s', line.rstrip()) |
|||
for idx, val in enumerate(markers[marker:]): |
|||
if val in line: |
|||
marker += idx + 1 |
|||
pbar.update(marker) |
|||
break |
|||
pbar.finish() |
|||
status = p.wait() |
|||
if status != 0: |
|||
raise SystemExit('Nonzero exit status: {!r}'.format(status)) |
|||
|
|||
|
|||
class Version (object): |
|||
'''A release version.''' |
|||
|
|||
RGX = re.compile( |
|||
r'^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(beta|rc)?([1-9]\d*))?$', |
|||
) |
|||
|
|||
@staticmethod |
|||
def parse_arg(text): |
|||
m = Version.RGX.match(text) |
|||
if m is None: |
|||
raise argparse.ArgumentTypeError( |
|||
'Could not parse version {!r} against regex {}'.format( |
|||
text, |
|||
Version.RGX.pattern, |
|||
), |
|||
) |
|||
else: |
|||
[major, minor, patch, _, betarc, hotfix] = m.groups() |
|||
return Version( |
|||
int(major), |
|||
int(minor), |
|||
int(patch), |
|||
betarc, |
|||
int(hotfix) if hotfix is not None else None, |
|||
) |
|||
|
|||
def __init__(self, major, minor, patch, betarc, hotfix): |
|||
for i in [major, minor, patch]: |
|||
assert type(i) is int, i |
|||
assert betarc in {None, 'rc', 'beta'}, betarc |
|||
assert hotfix is None or type(hotfix) is int, hotfix |
|||
if betarc is not None: |
|||
assert hotfix is not None, (betarc, hotfix) |
|||
|
|||
self.major = major |
|||
self.minor = minor |
|||
self.patch = patch |
|||
self.betarc = betarc |
|||
self.hotfix = hotfix |
|||
|
|||
if hotfix is None: |
|||
self.build = 50 |
|||
else: |
|||
assert hotfix > 0, hotfix |
|||
if betarc is None: |
|||
assert hotfix < 50, hotfix |
|||
self.build = 50 + hotfix |
|||
else: |
|||
assert hotfix < 26, hotfix |
|||
self.build = {'beta': 0, 'rc': 25}[betarc] + hotfix - 1 |
|||
|
|||
@property |
|||
def novtext(self): |
|||
return self._novtext(debian=False) |
|||
|
|||
@property |
|||
def vtext(self): |
|||
return 'v' + self.novtext |
|||
|
|||
@property |
|||
def debversion(self): |
|||
return self._novtext(debian=True) |
|||
|
|||
def _novtext(self, debian): |
|||
novtext = '{}.{}.{}'.format(self.major, self.minor, self.patch) |
|||
|
|||
if self.hotfix is None: |
|||
return novtext |
|||
else: |
|||
assert self.hotfix > 0, self.hotfix |
|||
if self.betarc is None: |
|||
assert self.hotfix < 50, self.hotfix |
|||
sep = '+' if debian else '-' |
|||
return '{}{}{}'.format(novtext, sep, self.hotfix) |
|||
else: |
|||
assert self.hotfix < 26, self.hotfix |
|||
sep = '~' if debian else '-' |
|||
return '{}{}{}{}'.format( |
|||
novtext, |
|||
sep, |
|||
self.betarc, |
|||
self.hotfix, |
|||
) |
|||
|
|||
def __repr__(self): |
|||
return '<Version {}>'.format(self.vtext) |
|||
|
|||
def _sort_tup(self): |
|||
if self.hotfix is None: |
|||
prio = 2 |
|||
else: |
|||
prio = {'beta': 0, 'rc': 1, None: 3}[self.betarc] |
|||
|
|||
return ( |
|||
self.major, |
|||
self.minor, |
|||
self.patch, |
|||
prio, |
|||
self.hotfix, |
|||
) |
|||
|
|||
def __cmp__(self, other): |
|||
return cmp(self._sort_tup(), other._sort_tup()) |
|||
|
|||
|
|||
class PathPatcher (object): |
|||
def __init__(self, path): |
|||
self._path = path |
|||
|
|||
def __enter__(self): |
|||
logging.debug('Patching %r', self._path) |
|||
self._inf = file(self._path, 'r') |
|||
self._outf = StringIO() |
|||
return (self._inf, self._outf) |
|||
|
|||
def __exit__(self, et, ev, tb): |
|||
if (et, ev, tb) == (None, None, None): |
|||
self._inf.close() |
|||
with file(self._path, 'w') as f: |
|||
f.write(self._outf.getvalue()) |
|||
|
|||
|
|||
# Unit Tests |
|||
class TestVersion (unittest.TestCase): |
|||
ValidVersionsAndBuilds = [ |
|||
# These are taken from: git tag --list | grep '^v1' |
|||
('v1.0.0-beta1', 0), |
|||
('v1.0.0-beta2', 1), |
|||
('v1.0.0-rc1', 25), |
|||
('v1.0.0-rc2', 26), |
|||
('v1.0.0-rc3', 27), |
|||
('v1.0.0-rc4', 28), |
|||
('v1.0.0', 50), |
|||
('v1.0.1', 50), |
|||
('v1.0.2', 50), |
|||
('v1.0.3', 50), |
|||
('v1.0.4', 50), |
|||
('v1.0.5', 50), |
|||
('v1.0.6', 50), |
|||
('v1.0.7-1', 51), |
|||
('v1.0.8', 50), |
|||
('v1.0.8-1', 51), |
|||
('v1.0.9', 50), |
|||
('v1.0.10', 50), |
|||
('v7.42.1000', 50), |
|||
] |
|||
|
|||
ValidVersions = [ |
|||
v |
|||
for (v, _) |
|||
in ValidVersionsAndBuilds |
|||
] |
|||
|
|||
def test_arg_parse_and_vtext_identity(self): |
|||
for case in self.ValidVersions: |
|||
v = Version.parse_arg(case) |
|||
self.assertEqual(v.vtext, case) |
|||
|
|||
def test_arg_parse_negatives(self): |
|||
cases = [ |
|||
'v07.0.0', |
|||
'v1.0.03', |
|||
'v1.2.3-0', # Hotfix numbers must begin w/ 1 |
|||
'v1.2.3~0', |
|||
'v1.2.3+0', |
|||
'1.2.3', |
|||
] |
|||
|
|||
for case in cases: |
|||
self.assertRaises( |
|||
argparse.ArgumentTypeError, |
|||
Version.parse_arg, |
|||
case, |
|||
) |
|||
|
|||
def test_version_sort(self): |
|||
expected = [Version.parse_arg(v) for v in self.ValidVersions] |
|||
|
|||
rng = random.Random() |
|||
rng.seed(0) |
|||
|
|||
for _ in range(1024): |
|||
vec = list(expected) |
|||
rng.shuffle(vec) |
|||
vec.sort() |
|||
self.assertEqual(vec, expected) |
|||
|
|||
def test_build_nums(self): |
|||
for (text, expected) in self.ValidVersionsAndBuilds: |
|||
version = Version.parse_arg(text) |
|||
self.assertEqual(version.build, expected) |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
if len(sys.argv) == 2 and sys.argv[1] == '--help': |
|||
main() |
|||
else: |
|||
actualargs = sys.argv |
|||
sys.argv = [sys.argv[0], '--verbose'] |
|||
|
|||
print '=== Self Test ===' |
|||
try: |
|||
unittest.main() |
|||
except SystemExit as e: |
|||
if e.args[0] != 0: |
|||
raise |
|||
|
|||
sys.argv = actualargs |
|||
print '=== Running ===' |
|||
main() |
Loading…
Reference in new issue