setup.py: make libbe._version optional.
[be.git] / 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 os
22 import os.path
23 import shutil
24 import string
25 import sys
26
27 from libbe.util.subproc import Pipe, invoke
28
29
30 INITIAL_COMMIT = '1bf1ec598b436f41ff27094eddf0b28c797e359d'
31
32
33 def validate_tag(tag):
34     """
35     >>> validate_tag('1.0.0')
36     >>> validate_tag('A.B.C-r7')
37     >>> validate_tag('A.B.C r7')
38     Traceback (most recent call last):
39       ...
40     Exception: Invalid character ' ' in tag 'A.B.C r7'
41     >>> validate_tag('"')
42     Traceback (most recent call last):
43       ...
44     Exception: Invalid character '"' in tag '"'
45     >>> validate_tag("'")
46     Traceback (most recent call last):
47       ...
48     Exception: Invalid character ''' in tag '''
49     """
50     for char in tag:
51         if char in string.digits:
52             continue
53         elif char in string.letters:
54             continue
55         elif char in ['.','-']:
56             continue
57         raise Exception("Invalid character '%s' in tag '%s'" % (char, tag))
58
59 def pending_changes():
60     """Use `git diff`s output to detect change.
61     """
62     status,stdout,stderr = invoke(['git', 'diff', 'HEAD'])
63     if len(stdout) == 0:
64         return False
65     return True
66
67 def set_release_version(tag):
68     print "set libbe.version._VERSION = '%s'" % tag
69     invoke(['sed', '-i', "s/^[# ]*_VERSION *=.*/_VERSION = '%s'/" % tag,
70             os.path.join('libbe', 'version.py')])
71
72 def remove_makefile_libbe_version_dependencies(filename):
73     print "set %s LIBBE_VERSION :=" % filename
74     invoke(['sed', '-i', "s/^LIBBE_VERSION *:=.*/LIBBE_VERSION :=/",
75             filename])
76
77 def commit(commit_message):
78     print 'commit current status:', commit_message
79     invoke(['git', 'commit', '-a', '-m', commit_message])
80
81 def tag(tag):
82     print 'tag current revision', tag
83     invoke(['git', 'tag', tag])
84
85 def export(target_dir):
86     if not target_dir.endswith(os.path.sep):
87         target_dir += os.path.sep
88     print 'export current revision to', target_dir
89     p = Pipe([['git', 'archive', '--prefix', target_dir, 'HEAD'],
90               ['tar', '-xv']])
91     assert p.status == 0, p.statuses
92
93 def make_version():
94     print 'generate libbe/_version.py'
95     invoke(['make', os.path.join('libbe', '_version.py')])
96
97 def make_changelog(filename, tag):
98     """Generate a ChangeLog from the git history.
99
100     Not the most ChangeLog-esque format, but iterating through commits
101     by hand is just too slow.
102     """
103     print 'generate ChangeLog file', filename, 'up to tag', tag
104     invoke(['git', 'log', '--no-merges',
105             '%s..%s' % (INITIAL_COMMIT, tag)],
106            stdout=open(filename, 'w')),
107
108 def set_vcs_name(be_dir, vcs_name='None'):
109     """Exported directory is not a git repository, so set vcs_name to
110     something that will work.
111       vcs_name: new_vcs_name
112     """
113     for directory in os.listdir(be_dir):
114         if not os.path.isdir(os.path.join(be_dir, directory)):
115             continue
116         filename = os.path.join(be_dir, directory, 'settings')
117         if os.path.exists(filename):
118             print 'set vcs_name in', filename, 'to', vcs_name
119             invoke(['sed', '-i', "s/^vcs_name:.*/vcs_name: %s/" % vcs_name,
120                     filename])
121
122 def make_id_cache():
123     """Generate .be/id-cache so users won't need to.
124     """
125     invoke(['./be', 'list'])
126
127 def create_tarball(tag):
128     release_name='be-%s' % tag
129     export_dir = release_name
130     export(export_dir)
131     make_version()
132     remove_makefile_libbe_version_dependencies(
133         os.path.join(export_dir, 'Makefile'))
134     print 'copy libbe/_version.py to %s/libbe/_version.py' % export_dir
135     shutil.copy(os.path.join('libbe', '_version.py'),
136                 os.path.join(export_dir, 'libbe', '_version.py'))
137     make_changelog(os.path.join(export_dir, 'ChangeLog'), tag)
138     make_id_cache()
139     print 'copy .be/id-cache to %s/.be/id-cache' % export_dir
140     shutil.copy(os.path.join('.be', 'id-cache'),
141                 os.path.join(export_dir, '.be', 'id-cache'))
142     set_vcs_name(os.path.join(export_dir, '.be'))
143     tarball_file = '%s.tar.gz' % release_name
144     print 'create tarball', tarball_file
145     invoke(['tar', '-czf', tarball_file, export_dir])
146     print 'remove', export_dir
147     shutil.rmtree(export_dir)
148
149 def test():
150     import doctest
151     doctest.testmod() 
152
153 if __name__ == '__main__':
154     import optparse
155     usage = """%prog [options] TAG
156
157 Create a git tag and a release tarball from the current revision.
158 For example
159   %prog 1.0.0
160
161 If you don't like what got committed, you can undo the release with
162   $ git tag -d 1.0.0
163   $ git reset --hard HEAD^
164 """
165     p = optparse.OptionParser(usage)
166     p.add_option('--test', dest='test', default=False,
167                  action='store_true', help='Run internal tests and exit')
168     options,args = p.parse_args()
169
170     if options.test == True:
171         test()
172         sys.exit(0)
173
174     assert len(args) == 1, '%d (!= 1) arguments: %s' % (len(args), args)
175     _tag = args[0]
176     validate_tag(_tag)
177
178     if pending_changes() == True:
179         print "Handle pending changes before releasing."
180         sys.exit(1)
181     set_release_version(_tag)
182     print "Update copyright information..."
183     env = dict(os.environ)
184     pythonpath = os.path.abspath('update-copyright')
185     if 'PYTHONPATH' in env:
186         env['PYTHONPATH'] = '{}:{}'.format(pythonpath, env['PYTHONPATH'])
187     else:
188         env['PYTHONPATH'] = pythonpath
189     status,stdout,stderr = invoke([
190             os.path.join('update-copyright', 'bin', 'update-copyright.py')],
191             env=env)
192     commit("Bumped to version %s" % _tag)
193     tag(_tag)
194     create_tarball(_tag)