#!/usr/bin/env python # Copyright (C) 2008, Red Hat, Inc. # Copyright (C) 2010 One Laptop per Child # # 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 2 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, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import gtk import optparse import os import re import subprocess import sys import StringIO upload_host = 'shell.sugarlabs.org' upload_root = '/upload/sources/sucrose' download_uri = 'http://download.sugarlabs.org/sources/sucrose/' announce_to = 'sugar-devel@lists.sugarlabs.org' class ReleaseReport(object): def __init__(self, version, last_tag): self._version = version self._last_tag = last_tag self.commits = '' def generate(self): release_tag = 'v' + self._version if self._last_tag: interval = self._last_tag + '..' + release_tag else: interval = release_tag p = subprocess.Popen(['git', 'log', interval, '--no-merges', '--pretty=format:%s (%an)'], stdout=subprocess.PIPE) lines = p.stdout.readlines() for line in lines: self.commits += '* ' + line class Release(object): def __init__(self): self._name = None self.version = None self.last_tag = None user, err_ = subprocess.Popen(['git', 'config', 'user.name'], stdout=subprocess.PIPE).communicate() email, err_ = subprocess.Popen(['git', 'config', 'user.email'], stdout=subprocess.PIPE).communicate() self.email = '%s <%s>' % (user.strip('\n'), email.strip('\n')) def read_config(self): config = open(self.config_path).read() m = re.search(self.name_regexp, config) self._name = m.group(1) m = re.search(self.version_regexp, config) self.version = m.group(1) def get_latest_tag(self): p = subprocess.Popen(['git', 'describe', '--tags', '--abbrev=0'], stdout=subprocess.PIPE) self.last_tag = p.stdout.read().strip('\n') def check_version(self): last_tag = self.last_tag if self.last_tag.startswith('v'): last_tag = last_tag[1:] if last_tag != self.version: print 'Warning: Tag (%s) does not match version number (%s)' \ % (last_tag, self.version) print 'You can use the --version option to force a specific version' sys.exit(1) def next_version(self, current): splitted = current.split('.') new_minor = int(splitted[-1]) + 1 splitted[-1] = str(new_minor) return '.'.join(splitted) def bump_version(self, version): config = open(self.config_path).read() m = re.search(self.version_regexp, config) if version is None: version = self.next_version(m.group(1)) config = config[:m.start(1)] + version + config[m.end(1):] open(self.config_path, "w").write(config) self.version = version def undo_version(self): subprocess.check_call(['git', 'checkout', self.config_path]) def tag(self): message = 'Release %s' % self.version subprocess.check_call(['git', 'commit', '-a', '-m' , '%s' % message]) subprocess.check_call(['git', 'tag', 'v%s' % self.version]) def push(self): subprocess.check_call(['git', 'push', 'origin', 'HEAD']) subprocess.check_call(['git', 'push', '--tags']) def build_tarball(self): ret = subprocess.call(self.tarball_command) return ret == 0 def get_tarball_name(self): return '%s-%s.tar.bz2' % (self._name.replace(' ', ''), self.version) def get_tarball_path(self): return self.get_tarball_name() def upload(self): upload_path = os.path.join(upload_root, self.path_differentiator, self._name.replace(' ', '')) upload_dest = upload_host + ':' + upload_path subprocess.check_call(['ssh', upload_host, 'mkdir', '-m', '775', '-p', upload_path]) subprocess.check_call(['scp', self.get_tarball_path(), upload_dest]) def announce(self): out = StringIO.StringIO() out.write('== Source ==\n\n') path = os.path.join(self.path_differentiator, self._name, self.get_tarball_name()) out.write(download_uri + path + '\n') report = ReleaseReport(self.version, self.last_tag) report.generate() if report.commits: out.write('\n== News ==\n\n') out.write(report.commits) announce = out.getvalue() out.close() dialog = gtk.Dialog() dialog.set_default_size(400, 400) dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 'Announce', gtk.RESPONSE_OK) scrolledwindow = gtk.ScrolledWindow() scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) text_view = gtk.TextView() text_view.get_buffer().set_text(announce) scrolledwindow.add_with_viewport(text_view) text_view.show() dialog.vbox.pack_start(scrolledwindow) scrolledwindow.show() if dialog.run() == gtk.RESPONSE_OK: buf = text_view.get_buffer() text = buf.get_text(buf.get_start_iter(), buf.get_end_iter()) subject = '[RELEASE] %s-%s' % (self._name, self.version) announce_filename = '.sugar-announce' f = open(announce_filename, 'w') f.write('From: %s\nTo: %s\nSubject: %s\n%s' % \ (self.email, announce_to, subject, text)) f.close() subprocess.check_call(['scp', announce_filename, upload_host + ':~']) subprocess.check_call(['ssh', upload_host, '/usr/sbin/sendmail', '-t', '<', announce_filename]) subprocess.check_call(['ssh', upload_host, 'rm', announce_filename]) os.unlink(announce_filename) class ActivityRelease(Release): def __init__(self): Release.__init__(self) self.config_path = os.path.join('activity', 'activity.info') self.name_regexp = 'name\s*=\s*(.*)' self.version_regexp = 'activity_version\s*=\s*(.*)' self.tarball_command = ['./setup.py', 'dist_source'] self.path_differentiator = 'fructose' def get_tarball_path(self): return os.path.join('dist', self.get_tarball_name()) class AutomakeRelease(Release): def __init__(self): Release.__init__(self) self.config_path = 'configure.ac' self.name_regexp = 'AC_INIT\(\[.*?\],\[.*?\],\[.*?\],\[(.*?)\]' self.version_regexp = 'AC_INIT\(\[.*?\],\[(.*?)\]' self.tarball_command = ['make', 'distcheck'] self.path_differentiator = 'glucose' def main(): parser = optparse.OptionParser() parser.add_option('-v', '--version', dest='version', help='Release version') (options, args_) = parser.parse_args() if os.path.exists('configure.ac'): release = AutomakeRelease() elif os.path.exists('setup.py'): release = ActivityRelease() else: print 'Unknown module type.' sys.exit(1) release.read_config() release.get_latest_tag() if options.version is None: release.check_version() print 'Bump version number...' release.bump_version(options.version) print 'Build source tarball...' if not release.build_tarball(): print 'Failed to build source tarball.' release.undo_version() sys.exit(1) print 'Tag the release in git...' release.tag() print 'Push the changes to remote git...' release.push() print 'Upload the source tarball...' release.upload() print 'Announce the release...' release.announce() main()