#!/usr/bin/env python # # Ravenbrook # # # RELEASE -- MAKE A RELEASE # # Gareth Rees, Ravenbrook Limited, 2014-03-18 # # # 1. INTRODUCTION # # This script automates the process of making a release, based on # [RELEASE-BUILD]. # # This script is idempotent: that is, you can run it repeatedly and it # will skip steps that have already been performed. from __future__ import unicode_literals import argparse from contextlib import contextmanager import datetime import os import re import subprocess import sys import p4 class Error(Exception): pass @contextmanager def pushdir(dir): """Context manager that changes directory to dir for the body of the with statement. """ cwd = os.getcwd() os.chdir(dir) yield os.chdir(cwd) DEPOT = '//info.ravenbrook.com' PROJECT_RE = r'[a-z][a-z0-9.-]*' PROJECT_FILESPEC_RE = r'{}/project/({})/'.format(re.escape(DEPOT), PROJECT_RE) VERSION_RE = r'\d+\.\d+' CUSTOMER_RE = r'[a-z][a-z0-9.-]*' BRANCH_RE = (r'master|(?:custom/({})/)?(?:main|version/({}))' .format(CUSTOMER_RE, VERSION_RE)) BRANCH_FILESPEC_RE = r'{}({})(?:/|$)'.format(PROJECT_FILESPEC_RE, BRANCH_RE) RELEASE_ENTRY = ''' {release} {today}
{branch}/...@{changelevel} {description} Known
Fixed ''' VERSION_ENTRY = ''' {release} ''' def main(argv): parser = argparse.ArgumentParser() parser.add_argument('-P', '--project', help='Name of the project.') parser.add_argument('-b', '--branch', help='Name of the branch to make the release from.') parser.add_argument('-C', '--changelevel', type=int, help='Changelevel at which to make the release.') parser.add_argument('-d', '--description', help='Description of the release.') parser.add_argument('-y', '--yes', action='store_true', help='Yes, really make the release.') args = parser.parse_args(argv[1:]) args.depot = DEPOT args.today = datetime.date.today().strftime('%Y-%m-%d') fmt = lambda s: s.format(**vars(args)) if not args.project: # Deduce project from current directory. filespec = next(p4.run('dirs', '.'))['dir'] m = re.match(PROJECT_FILESPEC_RE, filespec) if not m: raise Error("Can't deduce project from current directory.") args.project = m.group(1) print(fmt("project={project}")) if not any(p4.run('dirs', fmt('{depot}/project/{project}'))): raise Error(fmt("No such project: {project}")) if not args.branch: # Deduce branch from current directory. filespec = next(p4.run('dirs', '.'))['dir'] m = re.match(BRANCH_FILESPEC_RE, filespec) if not m: raise Error("Can't deduce branch from {}".format(filespec)) if args.project != m.group(1): raise Error("Specified project={} but current directory belongs " "to project={}.".format(args.project, m.group(1))) args.branch = m.group(2) print(fmt("branch={branch}")) m = re.match(BRANCH_RE, args.branch) if not m: raise Error(fmt("Invalid branch {branch}")) args.customer = m.group(1) args.version = m.group(2) if args.customer: print(fmt("customer={customer}")) if args.version: print(fmt("version={version}")) args.origin = fmt('{depot}/project/{project}/{branch}') if not any(p4.run('dirs', args.origin)): raise Error(fmt("No such branch: {branch}")) if not args.changelevel: cmd = p4.run('changes', '-m', '1', fmt('{origin}/...')) args.changelevel = int(next(cmd)['change']) print(fmt("changelevel={changelevel}")) # Deduce release from code/version.c. f = fmt('{origin}/code/version.c@{changelevel}') m = re.search(r'^#define MPS_RELEASE "release/((\d+\.\d+)\.\d+)"$', p4.contents(f), re.M) if not m: raise Error("Failed to extract release from {}.".format(f)) args.release = m.group(1) print(fmt("release={release}")) if args.version and args.version != m.group(2): raise Error(fmt("Version {version} does not match release {release}")) if args.customer: args.reldir = fmt('{depot}/project/{project}/custom/{customer}/release/{release}') else: args.reldir = fmt('{depot}/project/{project}/release/{release}') if not args.description: args.description = fmt("Release {release}.") print(fmt("description={description}")) args.kit = fmt('mps-kit-{release}') client_spec = dict( View0=fmt('{origin}/... //__CLIENT__/{kit}/...'), View1=fmt('{reldir}/... //__CLIENT__/release/{release}/...')) srcs = fmt('{origin}/...@{changelevel}') for line_end, args.ext, cmd in (('local', 'tar.gz', ['tar', 'czf']), ('win', 'zip', ['zip', '-r'])): client_spec['LineEnd'] = line_end archive = fmt('release/{release}/{kit}.{ext}') with p4.temp_client(client_spec) as (conn, client_root): try: conn.do('files', fmt('{reldir}/{kit}.{ext}')) except p4.Error as e: print("Adding {}".format(archive)) conn.do('sync', '-f', srcs) with pushdir(client_root): os.makedirs(fmt('release/{release}')) subprocess.check_call(cmd + [archive, args.kit], stdout=subprocess.DEVNULL) if not args.yes: print(fmt("--yes omitted: skipping submit of {kit}.{ext}")) else: conn.do('add', os.path.join(client_root, archive)) desc = fmt("Adding the MPS Kit {ext} archive for " "release {release}.") conn.do('submit', '-d', desc) else: print("{} already exists: skipping.".format(archive)) def register(filespec, search, replace): args.filespec = fmt(filespec) if p4.contents(args.filespec).find(args.release) != -1: print(fmt("{filespec} already updated: skipping.")) return client_spec = dict(View0=fmt('{filespec} //__CLIENT__/target')) with p4.temp_client(client_spec) as (conn, client_root): filename = os.path.join(client_root, 'target') conn.do('sync', filename) conn.do('edit', filename) with open(filename, encoding='utf8') as f: text = re.sub(search, fmt(replace), f.read(), 1) with open(filename, 'w', encoding='utf8') as f: f.write(text) for result in conn.run('diff'): if 'data' in result: print(result['data']) if args.yes: conn.do('submit', '-d', fmt("Registering release {release}."), filename) else: print(fmt("--yes omitted: skipping submit of {filespec}")) if not args.customer: register('{depot}/project/{project}/release/index.html', '(?<=\n)', RELEASE_ENTRY) register('{depot}/project/{project}/version/index.html', (r'(?<={0}\n \n)' .format(re.escape(args.version), re.escape(args.project))), VERSION_ENTRY) register('{depot}/project/{project}/index.rst', r'release/\d+\.\d+\.\d+', 'release/{release}') if __name__ == '__main__': main(sys.argv) # A. REFERENCES # # [RELEASE-BUILD] Richard Brooksby; "Memory Pool System Release Build # Procedure"; Ravenbrook Limited; 2002-06-17. # # # # B. DOCUMENT HISTORY # # 2014-03-18 GDR Created based on [RELEASE-BUILD]. # # # C. COPYRIGHT AND LICENCE # # Copyright (c) 2014 Ravenbrook Ltd. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the # distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # $Id$