summaryrefslogtreecommitdiff
path: root/tools/dist/release.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/dist/release.py')
-rwxr-xr-xtools/dist/release.py620
1 files changed, 427 insertions, 193 deletions
diff --git a/tools/dist/release.py b/tools/dist/release.py
index 5b12c00..141f937 100755
--- a/tools/dist/release.py
+++ b/tools/dist/release.py
@@ -41,7 +41,10 @@ import sys
import glob
import fnmatch
import shutil
-import urllib2
+try:
+ from urllib.request import urlopen # Python 3
+except:
+ from urllib2 import urlopen # Python 2
import hashlib
import tarfile
import logging
@@ -52,6 +55,7 @@ import itertools
import subprocess
import argparse # standard in Python 2.7
import io
+import yaml
import backport.status
@@ -67,78 +71,33 @@ except ImportError:
sys.path.remove(ezt_path)
+def get_dist_metadata_file_path():
+ return os.path.join(os.path.abspath(sys.path[0]), 'release-lines.yaml')
+
+# Read the dist metadata (about release lines)
+with open(get_dist_metadata_file_path(), 'r') as stream:
+ dist_metadata = yaml.safe_load(stream)
+
# Our required / recommended release tool versions by release branch
-tool_versions = {
- 'trunk' : {
- 'autoconf' : ['2.69',
- '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'],
- 'libtool' : ['2.4.6',
- 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'],
- 'swig' : ['3.0.12',
- '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'],
- },
- '1.13' : {
- 'autoconf' : ['2.69',
- '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'],
- 'libtool' : ['2.4.6',
- 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'],
- 'swig' : ['3.0.12',
- '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'],
- },
- '1.12' : {
- 'autoconf' : ['2.69',
- '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'],
- 'libtool' : ['2.4.6',
- 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'],
- 'swig' : ['3.0.12',
- '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'],
- },
- '1.11' : {
- 'autoconf' : ['2.69',
- '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'],
- 'libtool' : ['2.4.6',
- 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'],
- 'swig' : ['3.0.12',
- '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'],
- },
- '1.10' : {
- 'autoconf' : ['2.69',
- '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'],
- 'libtool' : ['2.4.6',
- 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'],
- 'swig' : ['3.0.12',
- '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'],
- },
- '1.9' : {
- 'autoconf' : ['2.69',
- '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'],
- 'libtool' : ['2.4.6',
- 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'],
- 'swig' : ['2.0.12',
- '65e13f22a60cecd7279c59882ff8ebe1ffe34078e85c602821a541817a4317f7'],
- },
- '1.8' : {
- 'autoconf' : ['2.69',
- '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'],
- 'libtool' : ['2.4.3',
- '36b4881c1843d7585de9c66c4c3d9a067ed3a3f792bc670beba21f5a4960acdf'],
- 'swig' : ['2.0.9',
- '586954000d297fafd7e91d1ad31089cc7e249f658889d11a44605d3662569539'],
- },
-}
+tool_versions = dist_metadata['tool_versions']
# The version that is our current recommended release
-# ### TODO: derive this from svn_version.h; see ../../build/getversion.py
-recommended_release = '1.12'
+recommended_release = dist_metadata['recommended_release']
# For clean-dist, a whitelist of artifacts to keep, by version.
-supported_release_lines = frozenset({"1.9", "1.10", "1.12", "1.13"})
+supported_release_lines = frozenset(dist_metadata['supported_release_lines'])
+# Long-Term Support (LTS) versions
+lts_release_lines = frozenset(dist_metadata['lts_release_lines'])
# Some constants
-svn_repos = 'https://svn.apache.org/repos/asf/subversion'
-dist_repos = 'https://dist.apache.org/repos/dist'
+svn_repos = os.getenv('SVN_RELEASE_SVN_REPOS',
+ 'https://svn.apache.org/repos/asf/subversion')
+dist_repos = os.getenv('SVN_RELEASE_DIST_REPOS',
+ 'https://dist.apache.org/repos/dist')
dist_dev_url = dist_repos + '/dev/subversion'
dist_release_url = dist_repos + '/release/subversion'
dist_archive_url = 'https://archive.apache.org/dist/subversion'
+buildbot_repos = os.getenv('SVN_RELEASE_BUILDBOT_REPOS',
+ 'https://svn.apache.org/repos/infra/infrastructure/buildbot/aegis/buildmaster')
KEYS = 'https://people.apache.org/keys/group/subversion.asc'
extns = ['zip', 'tar.gz', 'tar.bz2']
@@ -183,18 +142,6 @@ class Version(object):
def is_prerelease(self):
return self.pre != None
- def is_recommended(self):
- return self.branch == recommended_release
-
- def get_download_anchor(self):
- if self.is_prerelease():
- return 'pre-releases'
- else:
- if self.is_recommended():
- return 'recommended-release'
- else:
- return 'supported-releases'
-
def get_ver_tags(self, revnum):
# These get substituted into svn_version.h
ver_tag = ''
@@ -212,7 +159,7 @@ class Version(object):
ver_tag = '" (Nightly Build r%d)"' % revnum
ver_numtag = '"-nightly-r%d"' % revnum
else:
- ver_tag = '" (r%d)"' % revnum
+ ver_tag = '" (r%d)"' % revnum
ver_numtag = '""'
return (ver_tag, ver_numtag)
@@ -282,15 +229,12 @@ def get_exportdir(base_dir, version, revnum):
return os.path.join(get_tempdir(base_dir),
'subversion-%s-r%d' % (version, revnum))
-def get_deploydir(base_dir):
- return os.path.join(base_dir, 'deploy')
-
def get_target(args):
"Return the location of the artifacts"
if args.target:
return args.target
else:
- return get_deploydir(args.base_dir)
+ return os.path.join(args.base_dir, 'deploy')
def get_branch_path(args):
if not args.branch:
@@ -309,12 +253,14 @@ def get_tmplfile(filename):
return open(os.path.join(get_tmpldir(), filename))
except IOError:
# Hmm, we had a problem with the local version, let's try the repo
- return urllib2.urlopen(svn_repos + '/trunk/tools/dist/templates/' + filename)
+ return urlopen(svn_repos + '/trunk/tools/dist/templates/' + filename)
def get_nullfile():
return open(os.path.devnull, 'w')
-def run_script(verbose, script, hide_stderr=False):
+def run_command(cmd, verbose=True, hide_stderr=False, dry_run=False):
+ if verbose:
+ print("+ " + ' '.join(cmd))
stderr = None
if verbose:
stdout = None
@@ -323,23 +269,62 @@ def run_script(verbose, script, hide_stderr=False):
if hide_stderr:
stderr = get_nullfile()
+ if not dry_run:
+ subprocess.check_call(cmd, stdout=stdout, stderr=stderr)
+ else:
+ print(' ## dry-run; not executed')
+
+def run_script(verbose, script, hide_stderr=False):
for l in script.split('\n'):
- subprocess.check_call(l.split(), stdout=stdout, stderr=stderr)
+ run_command(l.split(), verbose, hide_stderr)
def download_file(url, target, checksum):
- response = urllib2.urlopen(url)
- target_file = open(target, 'w+')
+ """Download the file at URL to the local path TARGET.
+ If CHECKSUM is a string, verify the checksum of the downloaded
+ file and raise RuntimeError if it does not match. If CHECKSUM
+ is None, do not verify the downloaded file.
+ """
+ assert checksum is None or isinstance(checksum, str)
+
+ response = urlopen(url)
+ target_file = open(target, 'w+b')
target_file.write(response.read())
target_file.seek(0)
m = hashlib.sha256()
m.update(target_file.read())
target_file.close()
checksum2 = m.hexdigest()
- if checksum != checksum2:
+ if checksum is not None and checksum != checksum2:
raise RuntimeError("Checksum mismatch for '%s': "\
"downloaded: '%s'; expected: '%s'" % \
(target, checksum, checksum2))
+def run_svn(cmd, verbose=True, dry_run=False, username=None):
+ if (username):
+ cmd[:0] = ['--username', username]
+ run_command(['svn'] + cmd, verbose=verbose, dry_run=dry_run)
+
+def run_svnmucc(cmd, verbose=True, dry_run=False, username=None):
+ if (username):
+ cmd[:0] = ['--username', username]
+ run_command(['svnmucc'] + cmd, verbose=verbose, dry_run=dry_run)
+
+#----------------------------------------------------------------------
+def is_lts(version):
+ return version.branch in lts_release_lines
+
+def is_recommended(version):
+ return version.branch == recommended_release
+
+def get_download_anchor(version):
+ if version.is_prerelease():
+ return 'pre-releases'
+ else:
+ if is_recommended(version):
+ return 'recommended-release'
+ else:
+ return 'supported-releases'
+
#----------------------------------------------------------------------
# ezt helpers
@@ -363,7 +348,7 @@ def cleanup(args):
shutil.rmtree(get_prefix(args.base_dir), True)
shutil.rmtree(get_tempdir(args.base_dir), True)
- shutil.rmtree(get_deploydir(args.base_dir), True)
+ shutil.rmtree(get_target(args), True)
#----------------------------------------------------------------------
@@ -378,7 +363,8 @@ class RollDep(object):
def _test_version(self, cmd):
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ stderr=subprocess.STDOUT,
+ universal_newlines=True)
(stdout, stderr) = proc.communicate()
rc = proc.wait()
if rc: return ''
@@ -520,13 +506,236 @@ def build_env(args):
#----------------------------------------------------------------------
+# Create a new minor release branch
+
+def get_trunk_wc_path(base_dir, path=None):
+ trunk_wc_path = os.path.join(get_tempdir(base_dir), 'svn-trunk')
+ if path is None: return trunk_wc_path
+ return os.path.join(trunk_wc_path, path)
+
+def get_buildbot_wc_path(base_dir, path=None):
+ buildbot_wc_path = os.path.join(get_tempdir(base_dir), 'svn-buildmaster')
+ if path is None: return buildbot_wc_path
+ return os.path.join(buildbot_wc_path, path)
+
+def get_trunk_url(revnum=None):
+ return svn_repos + '/trunk' + '@' + (str(revnum) if revnum else '')
+
+def get_branch_url(ver):
+ return svn_repos + '/branches/' + ver.branch + '.x'
+
+def get_tag_url(ver):
+ return svn_repos + '/tags/' + ver.base
+
+def edit_file(path, pattern, replacement):
+ print("Editing '%s'" % (path,))
+ print(" pattern='%s'" % (pattern,))
+ print(" replace='%s'" % (replacement,))
+ old_text = open(path, 'r').read()
+ new_text = re.sub(pattern, replacement, old_text)
+ assert new_text != old_text
+ open(path, 'w').write(new_text)
+
+def edit_changes_file(path, newtext):
+ """Insert NEWTEXT in the 'CHANGES' file found at PATH,
+ just before the first line that starts with 'Version '.
+ """
+ print("Prepending to '%s'" % (path,))
+ print(" text='%s'" % (newtext,))
+ lines = open(path, 'r').readlines()
+ for i, line in enumerate(lines):
+ if line.startswith('Version '):
+ with open(path, 'w') as newfile:
+ newfile.writelines(lines[:i])
+ newfile.write(newtext)
+ newfile.writelines(lines[i:])
+ break
+
+#----------------------------------------------------------------------
+def make_release_branch(args):
+ ver = args.version
+ run_svn(['copy',
+ get_trunk_url(args.revnum),
+ get_branch_url(ver),
+ '-m', 'Create the ' + ver.branch + '.x release branch.'],
+ dry_run=args.dry_run)
+
+#----------------------------------------------------------------------
+def update_minor_ver_in_trunk(args):
+ """Change the minor version in trunk to the next (future) minor version.
+ """
+ ver = args.version
+ trunk_wc = get_trunk_wc_path(args.base_dir)
+ run_svn(['checkout',
+ get_trunk_url(args.revnum),
+ trunk_wc])
+
+ prev_ver = Version('1.%d.0' % (ver.minor - 1,))
+ next_ver = Version('1.%d.0' % (ver.minor + 1,))
+ relpaths = []
+
+ relpath = 'subversion/include/svn_version.h'
+ relpaths.append(relpath)
+ edit_file(get_trunk_wc_path(args.base_dir, relpath),
+ r'(#define SVN_VER_MINOR *)%s' % (ver.minor,),
+ r'\g<1>%s' % (next_ver.minor,))
+
+ relpath = 'subversion/tests/cmdline/svntest/main.py'
+ relpaths.append(relpath)
+ edit_file(get_trunk_wc_path(args.base_dir, relpath),
+ r'(SVN_VER_MINOR = )%s' % (ver.minor,),
+ r'\g<1>%s' % (next_ver.minor,))
+
+ relpath = 'subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java'
+ relpaths.append(relpath)
+ try:
+ # since r1817921 (just after branching 1.10)
+ edit_file(get_trunk_wc_path(args.base_dir, relpath),
+ r'SVN_VER_MINOR = %s;' % (ver.minor,),
+ r'SVN_VER_MINOR = %s;' % (next_ver.minor,))
+ except:
+ # before r1817921: two separate places
+ edit_file(get_trunk_wc_path(args.base_dir, relpath),
+ r'version.isAtLeast\(1, %s, 0\)' % (ver.minor,),
+ r'version.isAtLeast\(1, %s, 0\)' % (next_ver.minor,))
+ edit_file(get_trunk_wc_path(args.base_dir, relpath),
+ r'1.%s.0, but' % (ver.minor,),
+ r'1.%s.0, but' % (next_ver.minor,))
+
+ relpath = 'CHANGES'
+ relpaths.append(relpath)
+ # insert at beginning of CHANGES file
+ edit_changes_file(get_trunk_wc_path(args.base_dir, relpath),
+ 'Version ' + next_ver.base + '\n'
+ + '(?? ??? 20XX, from /branches/' + next_ver.branch + '.x)\n'
+ + get_tag_url(next_ver) + '\n'
+ + '\n')
+
+ log_msg = '''\
+Increment the trunk version number to %s, and introduce a new CHANGES
+section, following the creation of the %s.x release branch.
+
+* subversion/include/svn_version.h,
+ subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java,
+ subversion/tests/cmdline/svntest/main.py
+ (SVN_VER_MINOR): Increment to %s.
+
+* CHANGES: New section for %s.0.
+''' % (next_ver.branch, ver.branch, next_ver.minor, next_ver.branch)
+ commit_paths = [get_trunk_wc_path(args.base_dir, p) for p in relpaths]
+ run_svn(['commit'] + commit_paths + ['-m', log_msg],
+ dry_run=args.dry_run)
+
+#----------------------------------------------------------------------
+def create_status_file_on_branch(args):
+ ver = args.version
+ branch_wc = get_workdir(args.base_dir)
+ branch_url = get_branch_url(ver)
+ run_svn(['checkout', branch_url, branch_wc, '--depth=immediates'])
+
+ status_local_path = os.path.join(branch_wc, 'STATUS')
+ template_filename = 'STATUS.ezt'
+ data = { 'major-minor' : ver.branch,
+ 'major-minor-patch' : ver.base,
+ }
+
+ template = ezt.Template(compress_whitespace=False)
+ template.parse(get_tmplfile(template_filename).read())
+
+ with open(status_local_path, 'wx') as g:
+ template.generate(g, data)
+ run_svn(['add', status_local_path])
+ run_svn(['commit', status_local_path,
+ '-m', '* branches/' + ver.branch + '.x/STATUS: New file.'],
+ dry_run=args.dry_run)
+
+#----------------------------------------------------------------------
+def update_backport_bot(args):
+ ver = args.version
+ print("""\
+
+*** MANUAL STEP REQUIRED ***
+
+ Ask someone with appropriate access to add the %s.x branch
+ to the backport merge bot. See
+ http://subversion.apache.org/docs/community-guide/releasing.html#backport-merge-bot
+
+***
+
+""" % (ver.branch,))
+
+#----------------------------------------------------------------------
+def update_buildbot_config(args):
+ """Add the new branch to the list of branches monitored by the buildbot
+ master.
+ """
+ ver = args.version
+ buildbot_wc = get_buildbot_wc_path(args.base_dir)
+ run_svn(['checkout', buildbot_repos, buildbot_wc])
+
+ prev_ver = Version('1.%d.0' % (ver.minor - 1,))
+ next_ver = Version('1.%d.0' % (ver.minor + 1,))
+
+ relpath = 'master1/projects/subversion.conf'
+ edit_file(get_buildbot_wc_path(args.base_dir, relpath),
+ r'(MINOR_LINES=\[.*%s)(\])' % (prev_ver.minor,),
+ r'\1, %s\2' % (ver.minor,))
+
+ log_msg = '''\
+Subversion: start monitoring the %s branch.
+''' % (ver.branch)
+ commit_paths = [get_buildbot_wc_path(args.base_dir, relpath)]
+ run_svn(['commit'] + commit_paths + ['-m', log_msg],
+ dry_run=args.dry_run)
+
+#----------------------------------------------------------------------
+def create_release_branch(args):
+ make_release_branch(args)
+ update_minor_ver_in_trunk(args)
+ create_status_file_on_branch(args)
+ update_backport_bot(args)
+ update_buildbot_config(args)
+
+
+#----------------------------------------------------------------------
+def write_release_notes(args):
+
+ # Create a skeleton release notes file from template
+
+ template_filename = \
+ 'release-notes-lts.ezt' if is_lts(args.version) else 'release-notes.ezt'
+
+ prev_ver = Version('%d.%d.0' % (args.version.major, args.version.minor - 1))
+ data = { 'major-minor' : args.version.branch,
+ 'previous-major-minor' : prev_ver.branch,
+ }
+
+ template = ezt.Template(compress_whitespace=False)
+ template.parse(get_tmplfile(template_filename).read())
+
+ if args.edit_html_file:
+ with open(args.edit_html_file, 'w') as g:
+ template.generate(g, data)
+ else:
+ template.generate(sys.stdout, data)
+
+ # Add an "in progress" entry in the release notes index
+ #
+ index_file = os.path.normpath(args.edit_html_file + '/../index.html')
+ marker = '<ul id="release-notes-list">\n'
+ new_item = '<li><a href="%s.html">Subversion %s</a> – <i>in progress</i></li>\n' % (args.version.branch, args.version.branch)
+ edit_file(index_file,
+ re.escape(marker),
+ (marker + new_item).replace('\\', r'\\'))
+
+#----------------------------------------------------------------------
# Create release artifacts
def compare_changes(repos, branch, revision):
mergeinfo_cmd = ['svn', 'mergeinfo', '--show-revs=eligible',
repos + '/trunk/CHANGES',
repos + '/' + branch + '/' + 'CHANGES']
- stdout = subprocess.check_output(mergeinfo_cmd)
+ stdout = subprocess.check_output(mergeinfo_cmd, universal_newlines=True)
if stdout:
# Treat this as a warning since we are now putting entries for future
# minor releases in CHANGES on trunk.
@@ -544,7 +753,7 @@ def check_copyright_year(repos, branch, revision):
file_url = (repos + '/' + branch + '/'
+ branch_relpath + '@' + str(revision))
cat_cmd = ['svn', 'cat', file_url]
- stdout = subprocess.check_output(cat_cmd)
+ stdout = subprocess.check_output(cat_cmd, universal_newlines=True)
m = _copyright_re.search(stdout)
if m:
year = m.group('year')
@@ -596,17 +805,18 @@ def roll_tarballs(args):
compare_changes(svn_repos, branch, args.revnum)
# Ensure the output directory doesn't already exist
- if os.path.exists(get_deploydir(args.base_dir)):
+ if os.path.exists(get_target(args)):
raise RuntimeError('output directory \'%s\' already exists'
- % get_deploydir(args.base_dir))
+ % get_target(args))
- os.mkdir(get_deploydir(args.base_dir))
+ os.mkdir(get_target(args))
logging.info('Preparing working copy source')
shutil.rmtree(get_workdir(args.base_dir), True)
- run_script(args.verbose, 'svn checkout %s %s'
- % (svn_repos + '/' + branch + '@' + str(args.revnum),
- get_workdir(args.base_dir)))
+ run_svn(['checkout',
+ svn_repos + '/' + branch + '@' + str(args.revnum),
+ get_workdir(args.base_dir)],
+ verbose=args.verbose)
# Exclude stuff we don't want in the tarball, it will not be present
# in the exported tree.
@@ -617,8 +827,8 @@ def roll_tarballs(args):
exclude += ['packages', 'www']
cwd = os.getcwd()
os.chdir(get_workdir(args.base_dir))
- run_script(args.verbose,
- 'svn update --set-depth exclude %s' % " ".join(exclude))
+ run_svn(['update', '--set-depth=exclude'] + exclude,
+ verbose=args.verbose)
os.chdir(cwd)
if args.patches:
@@ -628,10 +838,10 @@ def roll_tarballs(args):
for name in os.listdir(args.patches):
if name.find(majmin) != -1 and name.endswith('patch'):
logging.info('Applying patch %s' % name)
- run_script(args.verbose,
- '''svn patch %s %s'''
- % (os.path.join(args.patches, name),
- get_workdir(args.base_dir)))
+ run_svn(['patch',
+ os.path.join(args.patches, name),
+ get_workdir(args.base_dir)],
+ verbose=args.verbose)
# Massage the new version number into svn_version.h.
ver_tag, ver_numtag = args.version.get_ver_tags(args.revnum)
@@ -666,11 +876,12 @@ def roll_tarballs(args):
def export(windows):
shutil.rmtree(exportdir, True)
if windows:
- eol_style = "--native-eol CRLF"
+ eol_style = "--native-eol=CRLF"
else:
- eol_style = "--native-eol LF"
- run_script(args.verbose, "svn export %s %s %s"
- % (eol_style, get_workdir(args.base_dir), exportdir))
+ eol_style = "--native-eol=LF"
+ run_svn(['export',
+ eol_style, get_workdir(args.base_dir), exportdir],
+ verbose=args.verbose)
def transform_sql():
for root, dirs, files in os.walk(exportdir):
@@ -744,25 +955,33 @@ def roll_tarballs(args):
for e in extns:
filename = basename + '.' + e
filepath = os.path.join(get_tempdir(args.base_dir), filename)
- shutil.move(filepath, get_deploydir(args.base_dir))
- filepath = os.path.join(get_deploydir(args.base_dir), filename)
+ shutil.move(filepath, get_target(args))
+ filepath = os.path.join(get_target(args), filename)
if args.version < Version("1.11.0-alpha1"):
# 1.10 and earlier generate *.sha1 files for compatibility reasons.
# They are deprecated, however, so we don't publicly link them in
# the announcements any more.
m = hashlib.sha1()
- m.update(open(filepath, 'r').read())
+ m.update(open(filepath, 'rb').read())
open(filepath + '.sha1', 'w').write(m.hexdigest())
m = hashlib.sha512()
- m.update(open(filepath, 'r').read())
+ m.update(open(filepath, 'rb').read())
open(filepath + '.sha512', 'w').write(m.hexdigest())
# Nightlies do not get tagged so do not need the header
if args.version.pre != 'nightly':
shutil.copy(os.path.join(get_workdir(args.base_dir),
'subversion', 'include', 'svn_version.h'),
- os.path.join(get_deploydir(args.base_dir),
- 'svn_version.h.dist-%s' % str(args.version)))
+ os.path.join(get_target(args),
+ 'svn_version.h.dist-%s'
+ % (str(args.version),)))
+
+ # Download and "tag" the KEYS file (in case a signing key is removed
+ # from a committer's LDAP profile down the road)
+ basename = 'subversion-%s.KEYS' % (str(args.version),)
+ filepath = os.path.join(get_tempdir(args.base_dir), basename)
+ download_file(KEYS, filepath, None)
+ shutil.move(filepath, get_target(args))
# And we're done!
@@ -804,14 +1023,12 @@ def post_candidates(args):
logging.info('Importing tarballs to %s' % dist_dev_url)
ver = str(args.version)
- svn_cmd = ['svn', 'import', '-m',
+ svn_cmd = ['import', '-m',
'Add Subversion %s candidate release artifacts' % ver,
'--auto-props', '--config-option',
'config:auto-props:*.asc=svn:eol-style=native;svn:mime-type=text/plain',
target, dist_dev_url]
- if (args.username):
- svn_cmd += ['--username', args.username]
- subprocess.check_call(svn_cmd)
+ run_svn(svn_cmd, verbose=args.verbose, username=args.username)
#----------------------------------------------------------------------
# Create tag
@@ -828,10 +1045,7 @@ def create_tag_only(args):
tag = svn_repos + '/tags/' + str(args.version)
- svnmucc_cmd = ['svnmucc', '-m',
- 'Tagging release ' + str(args.version)]
- if (args.username):
- svnmucc_cmd += ['--username', args.username]
+ svnmucc_cmd = ['-m', 'Tagging release ' + str(args.version)]
svnmucc_cmd += ['cp', str(args.revnum), branch_url, tag]
svnmucc_cmd += ['put', os.path.join(target, 'svn_version.h.dist' + '-' +
str(args.version)),
@@ -839,7 +1053,7 @@ def create_tag_only(args):
# don't redirect stdout/stderr since svnmucc might ask for a password
try:
- subprocess.check_call(svnmucc_cmd)
+ run_svnmucc(svnmucc_cmd, verbose=args.verbose, username=args.username)
except subprocess.CalledProcessError:
if args.version.is_prerelease():
logging.error("Do you need to pass --branch=trunk?")
@@ -878,7 +1092,8 @@ def bump_versions_on_branch(args):
args.version.patch + 1))
HEAD = subprocess.check_output(['svn', 'info', '--show-item=revision',
- '--', branch_url]).strip()
+ '--', branch_url],
+ universal_newlines=True).strip()
HEAD = int(HEAD)
def file_object_for(relpath):
fd = tempfile.NamedTemporaryFile()
@@ -898,13 +1113,14 @@ def bump_versions_on_branch(args):
svn_version_h.seek(0, os.SEEK_SET)
STATUS.seek(0, os.SEEK_SET)
- subprocess.check_call(['svnmucc', '-r', str(HEAD),
- '-m', 'Post-release housekeeping: '
- 'bump the %s branch to %s.'
- % (branch_url.split('/')[-1], str(new_version)),
- 'put', svn_version_h.name, svn_version_h.url,
- 'put', STATUS.name, STATUS.url,
- ])
+ run_svnmucc(['-r', str(HEAD),
+ '-m', 'Post-release housekeeping: '
+ 'bump the %s branch to %s.'
+ % (branch_url.split('/')[-1], str(new_version)),
+ 'put', svn_version_h.name, svn_version_h.url,
+ 'put', STATUS.name, STATUS.url,
+ ],
+ verbose=args.verbose, username=args.username)
del svn_version_h
del STATUS
@@ -924,7 +1140,8 @@ def clean_dist(args):
'''Clean the distribution directory of release artifacts of
no-longer-supported minor lines.'''
- stdout = subprocess.check_output(['svn', 'list', dist_release_url])
+ stdout = subprocess.check_output(['svn', 'list', dist_release_url],
+ universal_newlines=True)
def minor(version):
"""Return the minor release line of the parameter, which must be
@@ -946,10 +1163,8 @@ def clean_dist(args):
for i in sorted(to_keep):
logging.info("Saving release '%s'", i)
- svnmucc_cmd = ['svnmucc', '-m', 'Remove old Subversion releases.\n' +
+ svnmucc_cmd = ['-m', 'Remove old Subversion releases.\n' +
'They are still available at ' + dist_archive_url]
- if (args.username):
- svnmucc_cmd += ['--username', args.username]
for filename in filenames:
if Version(filename) not in to_keep:
logging.info("Removing %r", filename)
@@ -957,7 +1172,7 @@ def clean_dist(args):
# don't redirect stdout/stderr since svnmucc might ask for a password
if 'rm' in svnmucc_cmd:
- subprocess.check_call(svnmucc_cmd)
+ run_svnmucc(svnmucc_cmd, verbose=args.verbose, username=args.username)
else:
logging.info("Nothing to remove")
@@ -967,16 +1182,15 @@ def clean_dist(args):
def move_to_dist(args):
'Move candidate artifacts to the distribution directory.'
- stdout = subprocess.check_output(['svn', 'list', dist_dev_url])
+ stdout = subprocess.check_output(['svn', 'list', dist_dev_url],
+ universal_newlines=True)
filenames = []
for entry in stdout.split('\n'):
if fnmatch.fnmatch(entry, 'subversion-%s.*' % str(args.version)):
filenames.append(entry)
- svnmucc_cmd = ['svnmucc', '-m',
+ svnmucc_cmd = ['-m',
'Publish Subversion-%s.' % str(args.version)]
- if (args.username):
- svnmucc_cmd += ['--username', args.username]
svnmucc_cmd += ['rm', dist_dev_url + '/' + 'svn_version.h.dist'
+ '-' + str(args.version)]
for filename in filenames:
@@ -985,20 +1199,24 @@ def move_to_dist(args):
# don't redirect stdout/stderr since svnmucc might ask for a password
logging.info('Moving release artifacts to %s' % dist_release_url)
- subprocess.check_call(svnmucc_cmd)
+ run_svnmucc(svnmucc_cmd, verbose=args.verbose, username=args.username)
#----------------------------------------------------------------------
# Write announcements
def write_news(args):
'Write text for the Subversion website.'
- data = { 'date' : datetime.date.today().strftime('%Y%m%d'),
- 'date_pres' : datetime.date.today().strftime('%Y-%m-%d'),
+ if args.news_release_date:
+ release_date = datetime.datetime.strptime(args.news_release_date, '%Y-%m-%d')
+ else:
+ release_date = datetime.date.today()
+ data = { 'date' : release_date.strftime('%Y%m%d'),
+ 'date_pres' : release_date.strftime('%Y-%m-%d'),
'major-minor' : args.version.branch,
'version' : str(args.version),
'version_base' : args.version.base,
- 'anchor': args.version.get_download_anchor(),
- 'is_recommended': ezt_bool(args.version.is_recommended()),
+ 'anchor': get_download_anchor(args.version),
+ 'is_recommended': ezt_bool(is_recommended(args.version)),
'announcement_url': args.announcement_url,
}
@@ -1058,7 +1276,7 @@ def write_announcement(args):
'siginfo' : "\n".join(siginfo) + "\n",
'major-minor' : args.version.branch,
'major-minor-patch' : args.version.base,
- 'anchor' : args.version.get_download_anchor(),
+ 'anchor' : get_download_anchor(args.version),
}
if args.version.is_prerelease():
@@ -1153,9 +1371,9 @@ def get_siginfo(args, quiet=False):
% (n, filename, key_end))
sys.exit(1)
- fd, fn = tempfile.mkstemp()
- os.write(fd, key_start + key)
- os.close(fd)
+ fd, fn = tempfile.mkstemp(text=True)
+ with os.fdopen(fd, 'w') as key_file:
+ key_file.write(key_start + key)
verified = gpg.verify_file(open(fn, 'rb'), filename[:-4])
os.unlink(fn)
@@ -1177,6 +1395,7 @@ def get_siginfo(args, quiet=False):
gpg_output = subprocess.check_output(
['gpg', '--fixed-list-mode', '--with-colons', '--fingerprint', id],
stderr=subprocess.STDOUT,
+ universal_newlines=True,
)
gpg_output = gpg_output.splitlines()
@@ -1244,7 +1463,7 @@ def get_keys(args):
'Import the LDAP-based KEYS file to gpg'
# We use a tempfile because urlopen() objects don't have a .fileno()
with tempfile.SpooledTemporaryFile() as fd:
- fd.write(urllib2.urlopen(KEYS).read())
+ fd.write(urlopen(KEYS).read())
fd.flush()
fd.seek(0)
subprocess.check_call(['gpg', '--import'], stdin=fd)
@@ -1256,18 +1475,18 @@ def add_to_changes_dict(changes_dict, audience, section, change, revision):
if section:
section = section.lower()
change = change.strip()
-
+
if not audience in changes_dict:
changes_dict[audience] = dict()
if not section in changes_dict[audience]:
changes_dict[audience][section] = dict()
-
+
changes = changes_dict[audience][section]
if change in changes:
changes[change].add(revision)
else:
changes[change] = set([revision])
-
+
def print_section(changes_dict, audience, section, title, mandatory=False):
if audience in changes_dict:
audience_changes = changes_dict[audience]
@@ -1304,7 +1523,7 @@ def write_changelog(args):
# Putting [skip], [ignore], [c:skip] or [c:ignore] somewhere in the
# log message means this commit must be ignored for Changelog processing
# (ignored even with the --include-unlabeled-summaries option).
- #
+ #
# If there is no changes label anywhere in the commit message, and the
# --include-unlabeled-summaries option is used, we'll consider the summary
# line of the commit message (= first line except if it starts with a *)
@@ -1323,9 +1542,10 @@ def write_changelog(args):
previous = svn_repos + '/' + args.previous
include_unlabeled = args.include_unlabeled
separator_line = ('-' * 72) + '\n'
-
+
mergeinfo = subprocess.check_output(['svn', 'mergeinfo', '--show-revs',
- 'eligible', '--log', branch_url, previous])
+ 'eligible', '--log', branch_url, previous],
+ universal_newlines=True)
log_messages_dict = {
# This is a dictionary mapping revision numbers to their respective
# log messages. The expression in the "key:" part of the dict
@@ -1336,7 +1556,7 @@ def write_changelog(args):
for log_message in mergeinfo.split(separator_line)[1:-1]
}
mergeinfo = mergeinfo.splitlines()
-
+
separator_pattern = re.compile('^-{72}$')
revline_pattern = re.compile('^r(\d+) \| [^\|]+ \| [^\|]+ \| \d+ lines?$')
changes_prefix_pattern = re.compile(r'^\[(U|D)?:?([^\]]+)?\](.+)$')
@@ -1353,7 +1573,7 @@ def write_changelog(args):
audience = None
section = None
message = None
-
+
for line in mergeinfo:
if separator_pattern.match(line):
# New revision section. Reset variables.
@@ -1370,7 +1590,7 @@ def write_changelog(args):
# logic, in order to extract CHANGES_PREFIX_PATTERN
# and CHANGES_SUFFIX_PATTERN lines from the trunk log
# message.
-
+
# 2. Parse the STATUS entry
this_log_message = log_messages_dict[revision]
status_paragraph = this_log_message.split('\n\n')[2]
@@ -1411,7 +1631,7 @@ def write_changelog(args):
if re.search(r'\[(c:)?(skip|ignore)\]', line, re.IGNORECASE):
changes_ignore = True
-
+
prefix_match = changes_prefix_pattern.match(line)
if prefix_match:
audience = prefix_match.group(1)
@@ -1459,17 +1679,25 @@ def main():
parser = argparse.ArgumentParser(
description='Create an Apache Subversion release.')
parser.add_argument('--clean', action='store_true', default=False,
- help='Remove any directories previously created by %(prog)s')
+ help='''Remove any directories previously created by %(prog)s,
+ including the 'prefix' dir, the 'temp' dir, and the
+ default or specified target dir.''')
parser.add_argument('--verbose', action='store_true', default=False,
help='Increase output verbosity')
parser.add_argument('--base-dir', default=os.getcwd(),
help='''The directory in which to create needed files and
folders. The default is the current working
directory.''')
+ parser.add_argument('--target',
+ help='''The full path to the directory containing
+ release artifacts. Default: <BASE_DIR>/deploy''')
parser.add_argument('--branch',
help='''The branch to base the release on,
as a path relative to ^/subversion/.
Default: 'branches/MAJOR.MINOR.x'.''')
+ parser.add_argument('--username',
+ help='Username for committing to ' + svn_repos +
+ ' or ' + dist_repos + '.')
subparsers = parser.add_subparsers(title='subcommands')
# Setup the parser for the build-env subcommand
@@ -1487,6 +1715,40 @@ def main():
help='''Attempt to use existing build dependencies before
downloading and building a private set.''')
+ # Setup the parser for the create-release-branch subcommand
+ subparser = subparsers.add_parser('create-release-branch',
+ help='''Create a minor release branch: branch from trunk,
+ update version numbers on trunk, create status
+ file on branch, update backport bot,
+ update buildbot config.''')
+ subparser.set_defaults(func=create_release_branch)
+ subparser.add_argument('version', type=Version,
+ help='''A version number to indicate the branch, such as
+ '1.7.0' (the '.0' is required).''')
+ subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')),
+ nargs='?', default=None,
+ help='''The trunk revision number to base the branch on.
+ Default is HEAD.''')
+ subparser.add_argument('--dry-run', action='store_true', default=False,
+ help='Avoid committing any changes to repositories.')
+
+ # Setup the parser for the create-release-branch subcommand
+ subparser = subparsers.add_parser('write-release-notes',
+ help='''Write a template release-notes file.''')
+ subparser.set_defaults(func=write_release_notes)
+ subparser.add_argument('version', type=Version,
+ help='''A version number to indicate the branch, such as
+ '1.7.0' (the '.0' is required).''')
+ subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')),
+ nargs='?', default=None,
+ help='''The trunk revision number to base the branch on.
+ Default is HEAD.''')
+ subparser.add_argument('--edit-html-file',
+ help='''Write the template release-notes to this file,
+ and update 'index.html' in the same directory.''')
+ subparser.add_argument('--dry-run', action='store_true', default=False,
+ help='Avoid committing any changes to repositories.')
+
# Setup the parser for the roll subcommand
subparser = subparsers.add_parser('roll',
help='''Create the release artifacts.''')
@@ -1504,9 +1766,6 @@ def main():
subparser.set_defaults(func=sign_candidates)
subparser.add_argument('version', type=Version,
help='''The release label, such as '1.7.0-alpha1'.''')
- subparser.add_argument('--target',
- help='''The full path to the directory containing
- release artifacts.''')
subparser.add_argument('--userid',
help='''The (optional) USER-ID specifying the key to be
used for signing, such as '110B1C95' (Key-ID). If
@@ -1519,11 +1778,6 @@ def main():
subparser.set_defaults(func=post_candidates)
subparser.add_argument('version', type=Version,
help='''The release label, such as '1.7.0-alpha1'.''')
- subparser.add_argument('--username',
- help='''Username for ''' + dist_repos + '''.''')
- subparser.add_argument('--target',
- help='''The full path to the directory containing
- release artifacts.''')
# Setup the parser for the create-tag subcommand
subparser = subparsers.add_parser('create-tag',
@@ -1534,11 +1788,6 @@ def main():
help='''The release label, such as '1.7.0-alpha1'.''')
subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')),
help='''The revision number to base the release on.''')
- subparser.add_argument('--username',
- help='''Username for ''' + svn_repos + '''.''')
- subparser.add_argument('--target',
- help='''The full path to the directory containing
- release artifacts.''')
# Setup the parser for the bump-versions-on-branch subcommand
subparser = subparsers.add_parser('bump-versions-on-branch',
@@ -1548,11 +1797,6 @@ def main():
help='''The release label, such as '1.7.0-alpha1'.''')
subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')),
help='''The revision number to base the release on.''')
- subparser.add_argument('--username',
- help='''Username for ''' + svn_repos + '''.''')
- subparser.add_argument('--target',
- help='''The full path to the directory containing
- release artifacts.''')
# The clean-dist subcommand
subparser = subparsers.add_parser('clean-dist',
@@ -1560,8 +1804,6 @@ def main():
subparser.set_defaults(func=clean_dist)
subparser.add_argument('--dist-dir',
help='''The directory to clean.''')
- subparser.add_argument('--username',
- help='''Username for ''' + dist_repos + '''.''')
# The move-to-dist subcommand
subparser = subparsers.add_parser('move-to-dist',
@@ -1571,8 +1813,6 @@ def main():
subparser.set_defaults(func=move_to_dist)
subparser.add_argument('version', type=Version,
help='''The release label, such as '1.7.0-alpha1'.''')
- subparser.add_argument('--username',
- help='''Username for ''' + dist_repos + '''.''')
# The write-news subcommand
subparser = subparsers.add_parser('write-news',
@@ -1581,6 +1821,9 @@ def main():
subparser.set_defaults(func=write_news)
subparser.add_argument('--announcement-url',
help='''The URL to the archived announcement email.''')
+ subparser.add_argument('--news-release-date',
+ help='''The release date for the news, as YYYY-MM-DD.
+ Default: today.''')
subparser.add_argument('--edit-html-file',
help='''Insert the text into this file
news.html, index.html).''')
@@ -1595,9 +1838,6 @@ def main():
subparser.add_argument('--security', action='store_true', default=False,
help='''The release being announced includes security
fixes.''')
- subparser.add_argument('--target',
- help='''The full path to the directory containing
- release artifacts.''')
subparser.add_argument('version', type=Version,
help='''The release label, such as '1.7.0-alpha1'.''')
@@ -1606,9 +1846,6 @@ def main():
help='''Output to stdout template text for the download
table for subversion.apache.org''')
subparser.set_defaults(func=write_downloads)
- subparser.add_argument('--target',
- help='''The full path to the directory containing
- release artifacts.''')
subparser.add_argument('version', type=Version,
help='''The release label, such as '1.7.0-alpha1'.''')
@@ -1619,9 +1856,6 @@ def main():
subparser.set_defaults(func=check_sigs)
subparser.add_argument('version', type=Version,
help='''The release label, such as '1.7.0-alpha1'.''')
- subparser.add_argument('--target',
- help='''The full path to the directory containing
- release artifacts.''')
# get-keys
subparser = subparsers.add_parser('get-keys',
@@ -1641,7 +1875,7 @@ def main():
like [U:client], [D:api], [U], ...''')
subparser.set_defaults(func=write_changelog)
subparser.add_argument('previous',
- help='''The "previous" branch or tag, relative to
+ help='''The "previous" branch or tag, relative to
^/subversion/, to compare "branch" against.''')
subparser.add_argument('--include-unlabeled-summaries',
dest='include_unlabeled',
@@ -1652,7 +1886,7 @@ def main():
summary line contains 'STATUS', 'CHANGES',
'Post-release housekeeping', 'Follow-up' or starts
with '*').''')
-
+
# Parse the arguments
args = parser.parse_args()