/ release.py
release.py
1 #!/usr/bin/python 2 # 3 # Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org> 4 # W. Trevor King <wking@tremily.us> 5 # 6 # This file is part of Bugs Everywhere. 7 # 8 # Bugs Everywhere is free software: you can redistribute it and/or modify it 9 # under the terms of the GNU General Public License as published by the Free 10 # Software Foundation, either version 2 of the License, or (at your option) any 11 # later version. 12 # 13 # Bugs Everywhere is distributed in the hope that it will be useful, but 14 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 16 # more details. 17 # 18 # You should have received a copy of the GNU General Public License along with 19 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>. 20 21 import codecs 22 import optparse 23 import os 24 import os.path 25 import shutil 26 import string 27 import sys 28 29 from libbe.util.subproc import invoke 30 31 32 INITIAL_COMMIT = '1bf1ec598b436f41ff27094eddf0b28c797e359d' 33 34 35 def validate_tag(tag): 36 """ 37 >>> validate_tag('1.0.0') 38 >>> validate_tag('A.B.C-r7') 39 >>> validate_tag('A.B.C r7') 40 Traceback (most recent call last): 41 ... 42 Exception: Invalid character ' ' in tag 'A.B.C r7' 43 >>> validate_tag('"') 44 Traceback (most recent call last): 45 ... 46 Exception: Invalid character '"' in tag '"' 47 >>> validate_tag("'") 48 Traceback (most recent call last): 49 ... 50 Exception: Invalid character ''' in tag ''' 51 """ 52 for char in tag: 53 if char in string.digits: 54 continue 55 elif char in string.letters: 56 continue 57 elif char in ['.','-']: 58 continue 59 raise Exception("Invalid character '%s' in tag '%s'" % (char, tag)) 60 61 def pending_changes(): 62 """Use `git diff`s output to detect change. 63 """ 64 status,stdout,stderr = invoke(['git', 'diff', 'HEAD']) 65 if len(stdout) == 0: 66 return False 67 return True 68 69 def set_release_version(tag): 70 print "set libbe.version._VERSION = '%s'" % tag 71 invoke(['sed', '-i', "s/^[# ]*_VERSION *=.*/_VERSION = '%s'/" % tag, 72 os.path.join('libbe', 'version.py')]) 73 74 def remove_makefile_libbe_version_dependencies(filename): 75 print "set %s LIBBE_VERSION :=" % filename 76 invoke(['sed', '-i', "s/^LIBBE_VERSION *:=.*/LIBBE_VERSION :=/", 77 filename]) 78 79 def commit(commit_message): 80 print 'commit current status:', commit_message 81 invoke(['git', 'commit', '-a', '-m', commit_message]) 82 83 def tag(tag): 84 print 'tag current revision', tag 85 invoke(['git', 'tag', '-s', '-m', 'version {}'.format(tag), tag]) 86 87 def export(target_dir): 88 if not target_dir.endswith(os.path.sep): 89 target_dir += os.path.sep 90 print 'export current revision to', target_dir 91 status,stdout,stderr = invoke( 92 ['git', 'archive', '--prefix', target_dir, 'HEAD'], 93 unicode_output=False) 94 status,stdout,stderr = invoke(['tar', '-xv'], stdin=stdout) 95 96 def make_version(): 97 print 'generate libbe/_version.py' 98 invoke(['make', os.path.join('libbe', '_version.py')]) 99 100 def make_changelog(filename, tag): 101 """Generate a ChangeLog from the git history. 102 103 Not the most ChangeLog-esque format, but iterating through commits 104 by hand is just too slow. 105 """ 106 print 'generate ChangeLog file', filename, 'up to tag', tag 107 status,stdout,stderr = invoke( 108 ['git', 'log', '--no-merges', '{}..{}'.format(INITIAL_COMMIT, tag)]) 109 with codecs.open(filename, 'w', 'utf-8') as f: 110 for line in stdout.splitlines(): 111 f.write(line.rstrip()) 112 f.write(u'\n') 113 114 def set_vcs_name(be_dir, vcs_name='None'): 115 """Exported directory is not a git repository, so set vcs_name to 116 something that will work. 117 vcs_name: new_vcs_name 118 """ 119 for directory in os.listdir(be_dir): 120 if not os.path.isdir(os.path.join(be_dir, directory)): 121 continue 122 filename = os.path.join(be_dir, directory, 'settings') 123 if os.path.exists(filename): 124 print 'set vcs_name in', filename, 'to', vcs_name 125 invoke(['sed', '-i', "s/^vcs_name:.*/vcs_name: %s/" % vcs_name, 126 filename]) 127 128 def make_id_cache(): 129 """Generate .be/id-cache so users won't need to. 130 """ 131 invoke([sys.executable, './be', 'list']) 132 133 def make_html_docs(docdir): 134 """Generate docs so users won't need to install Sphinx, etc. 135 """ 136 print('generate HTML docs in {}'.format(docdir)) 137 status,stdout,stderr = invoke( 138 ['make', 'SPHINXBUILD=sphinx-build-2.7', 'dirhtml'], cwd=docdir) 139 140 def create_tarball(tag): 141 release_name='be-%s' % tag 142 export_dir = release_name 143 export(export_dir) 144 make_version() 145 remove_makefile_libbe_version_dependencies( 146 os.path.join(export_dir, 'Makefile')) 147 print 'copy libbe/_version.py to %s/libbe/_version.py' % export_dir 148 shutil.copy(os.path.join('libbe', '_version.py'), 149 os.path.join(export_dir, 'libbe', '_version.py')) 150 make_changelog(os.path.join(export_dir, 'ChangeLog'), tag) 151 make_id_cache() 152 make_html_docs(os.path.join(export_dir, 'doc')) 153 print 'copy .be/id-cache to %s/.be/id-cache' % export_dir 154 shutil.copy(os.path.join('.be', 'id-cache'), 155 os.path.join(export_dir, '.be', 'id-cache')) 156 set_vcs_name(os.path.join(export_dir, '.be')) 157 tarball_file = '%s.tar.gz' % release_name 158 print 'create tarball', tarball_file 159 invoke(['tar', '-czf', tarball_file, export_dir]) 160 print 'remove', export_dir 161 shutil.rmtree(export_dir) 162 163 def test(): 164 import doctest 165 doctest.testmod() 166 167 def main(*args, **kwargs): 168 usage = """%prog [options] TAG 169 170 Create a git tag and a release tarball from the current revision. 171 For example 172 %prog 1.0.0 173 174 If you don't like what got committed, you can undo the release with 175 $ git tag -d 1.0.0 176 $ git reset --hard HEAD^ 177 """ 178 p = optparse.OptionParser(usage) 179 p.add_option('--test', dest='test', default=False, 180 action='store_true', help='Run internal tests and exit') 181 options,args = p.parse_args(*args, **kwargs) 182 183 if options.test == True: 184 test() 185 sys.exit(0) 186 187 assert len(args) == 1, '%d (!= 1) arguments: %s' % (len(args), args) 188 _tag = args[0] 189 validate_tag(_tag) 190 191 if pending_changes() == True: 192 print "Handle pending changes before releasing." 193 sys.exit(1) 194 set_release_version(_tag) 195 print "Update copyright information..." 196 env = dict(os.environ) 197 pythonpath = os.path.abspath('update-copyright') 198 if 'PYTHONPATH' in env: 199 env['PYTHONPATH'] = '{}:{}'.format(pythonpath, env['PYTHONPATH']) 200 else: 201 env['PYTHONPATH'] = pythonpath 202 status,stdout,stderr = invoke([ 203 os.path.join('update-copyright', 'bin', 'update-copyright.py')], 204 env=env) 205 commit("Bumped to version %s" % _tag) 206 tag(_tag) 207 create_tarball(_tag) 208 209 210 if __name__ == '__main__': 211 main()