#!/usr/bin/env python2 # Copyright 2016 Lars Wirzenius # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # =*= License: GPL-3+ =*= import os import re import stat import time import cliapp import yaml import bumperlib class Bumper(cliapp.Application): def process_args(self, args): project_name = self.get_project_name() version = self.get_version(args) print 'Releasing {} version {}'.format(project_name, version) current_version = self.get_current_version() if not self.version_is_newer(version, current_version): raise cliapp.AppException( 'New version {} is older than current version {}'.format( version, current_version)) version_files = [ ('version.py', self.write_version_py), ('version.txt', self.write_version_txt), ('version.yaml', self.write_version_yaml), ] for basename, func in version_files: filename = self.find_version_file(basename) if filename: print '.. {}'.format(filename) func(filename, version, '') print '... debian/changelog' self.update_debian_changelog(version, '') self.release_debian_changelog() print '... NEWS' self.update_NEWS_for_release(version) print '... git commit' self.commit(version, 'Prepare to release version {}'.format(version)) print '... git tag' self.make_release_tag(version) gitversion = version + '+git' print 'Updating in-development version to', gitversion for basename, func in version_files: filename = self.find_version_file(basename) if filename: print '... {}'.format(filename) func(filename, version, '+git') print '... debian/changelog' self.update_debian_changelog(version, 'New upstream version.') self.update_debian_changelog(gitversion, '') print '... NEWS' self.update_NEWS_for_git(gitversion) print '... git commit' self.commit( version, 'Bump version number post-release to {}'.format(gitversion)) def get_version(self, args): if len(args) != 1: raise cliapp.AppException('Need exactly one argument, the version') return args[0] def find_version_file(self, basename): output = cliapp.runcmd(['git', 'ls-files']) filenames = [x.strip() for x in output.splitlines()] version_files = [ x for x in filenames if x == basename or x.endswith('/' + basename) ] if len(version_files) == 0: return None elif len(version_files) > 1: raise cliapp.AppException('Too many {} in project'.format(basename)) return version_files[0] def run_setup(self, *args): prefix = None # If we can run setup.py directly, we do. This takes care of Py2 vs Py3. st = os.lstat('setup.py') if (stat.S_IMODE(st.st_mode) & stat.S_IXUSR) == stat.S_IXUSR: prefix = ['./setup.py'] # Is Python3 mentioned? If so, assume setup.py needs it. text = open('setup.py').read() if prefix is None and text and 'python3' in text: prefix = ['python3', 'setup.py'] if prefix is None: prefix = ['python', 'setup.py'] return cliapp.runcmd(prefix + list(args)).strip() def get_current_version(self): return self.run_setup('--version') def version_is_newer(self, v1, v2): '''Is v1 newer than v2?''' vi1 = self.parse_version_info(v1, None) vi2 = self.parse_version_info(v2, None) returncode, out, err = cliapp.runcmd_unchecked( ['dpkg', '--compare-versions', v1, 'gt', v2]) return returncode == 0 def update_debian_changelog(self, version, msg): debian_version = '{}-1'.format(version) cliapp.runcmd(['dch', '-v', debian_version, msg]) def release_debian_changelog(self): cliapp.runcmd(['dch', '-r', '']) def update_NEWS_for_release(self, version): with open('NEWS') as f: text = f.read() date = time.strftime('%Y-%m-%d') pattern = r'^Version \d+(\.\d+)*(\+git)?, not yet released$' replacement = 'Version {}, released {}'.format(version, date) updated = re.sub(pattern, replacement, text, count=1, flags=re.M) with open('NEWS', 'w') as f: f.write(updated) def update_NEWS_for_git(self, version): with open('NEWS') as f: text = f.read() pattern = r'^Version \d+(\.\d+)*, released \d\d\d\d-\d\d-\d\d$' replacement = 'Version {}, not yet released'.format(version) match = re.search(pattern, text, flags=re.M) if not match: raise cliapp.AppException('No place to insert new entry in NEWS') before, after = text[:match.start()], text[match.start():] with open('NEWS', 'w') as f: f.write(before) f.write(replacement + '\n') f.write('-' * len(replacement)) f.write('\n\n\n') f.write(after) def commit(self, version, msg): cliapp.runcmd(['git', 'commit', '-am', msg]) def make_release_tag(self, version): name = self.get_project_name() tag_name = '{}-{}'.format(name, version) msg = 'Release version {}'.format(version) cliapp.runcmd(['git', 'tag', '-sam', msg, tag_name]) def get_project_name(self): return self.run_setup('--name') def write_version_py(self, filename, version, suffix): version_info = self.parse_version_info(version, suffix) with open(filename, 'w') as f: f.write('__version__ = "{}"\n'.format(version + suffix)) f.write('__version_info__ = {!r}\n'.format(version_info)) def parse_version_info(self, version, suffix): parts = version.split('.') result = [] for part in parts: try: result.append(int(part)) except ValueError: result.append(part) if suffix: result.append(suffix) return tuple(result) def write_version_txt(self, filename, version, suffix): with open(filename, 'w') as f: f.write('{}\n'.format(version + suffix)) def write_version_yaml(self, filename, version, suffix): version = { 'version': version + suffix, 'version_info': self.parse_version_info(version, suffix), } with open(filename, 'w') as f: yaml.safe_dump(version, stream=f, indent=4) Bumper(version=bumperlib.__version__).run()