/ 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()