Start development series 0.1-post
[0release.git] / release.py
blob2f802ddce47e7a11a423669cd6c084f162154d7f
1 # Copyright (C) 2007, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import os, sys, subprocess, shutil, tarfile, tempfile
5 from zeroinstall import SafeException
6 from zeroinstall.injector import reader, model
7 from logging import info
8 from scm import GIT
10 release_status_file = 'release-status'
12 def run_unit_tests(impl):
13 print "SKIPPING unit tests for %s (sorry - not yet implemented!)" % impl
15 def show_and_run(cmd, args):
16 print "Executing: %s %s" % (cmd, ' '.join("[%s]" % x for x in args))
17 subprocess.check_call(['sh', '-c', cmd, '-'] + args)
19 def suggest_release_version(snapshot_version):
20 """Given a snapshot version, suggest a suitable release version.
21 >>> suggest_release_version('1.0-pre')
22 '1.0'
23 >>> suggest_release_version('0.9-post')
24 '0.10'
25 >>> suggest_release_version('3')
26 Traceback (most recent call last):
27 ...
28 SafeException: Version '3' is not a snapshot version (should end in -pre or -post)
29 """
30 version = model.parse_version(snapshot_version)
31 mod = version[-1]
32 if mod == 0:
33 raise SafeException("Version '%s' is not a snapshot version (should end in -pre or -post)" % snapshot_version)
34 if mod > 0:
35 # -post, so increment the number
36 version[-2][-1] += 1
37 version[-1] = 0 # Remove the modifier
38 return model.format_version(version)
40 def publish(iface, **kwargs):
41 args = [os.environ['0PUBLISH']]
42 for k in kwargs:
43 value = kwargs[k]
44 if value is True:
45 args += ['--' + k.replace('_', '-')]
46 elif value is not None:
47 args += ['--' + k.replace('_', '-'), value]
48 args.append(iface)
49 info("Executing %s", args)
50 subprocess.check_call(args)
52 def get_singleton_impl(iface):
53 impls = iface.implementations
54 if len(impls) != 1:
55 raise SafeException("Local feed '%s' contains %d versions! I need exactly one!" % (iface.uri, len(impls)))
56 return impls.values()[0]
58 def backup_if_exists(name):
59 if not os.path.exists(name):
60 return
61 backup = name + '~'
62 if os.path.exists(backup):
63 print "(deleting old backup %s)" % backup
64 if os.path.isdir(backup):
65 shutil.rmtree(backup)
66 else:
67 os.unlink(backup)
68 os.rename(name, backup)
69 print "(renamed old %s as %s; will delete on next run)" % (name, backup)
71 class Status(object):
72 __slots__ = ['old_snapshot_version', 'release_version', 'head_before_release', 'new_snapshot_version', 'head_at_release', 'created_archive', 'tagged']
73 def __init__(self):
74 for name in self.__slots__:
75 setattr(self, name, None)
77 if os.path.isfile(release_status_file):
78 for line in file(release_status_file):
79 assert line.endswith('\n')
80 line = line[:-1]
81 name, value = line.split('=')
82 setattr(self, name, value)
83 info("Loaded status %s=%s", name, value)
85 def save(self):
86 tmp_name = release_status_file + '.new'
87 tmp = file(tmp_name, 'w')
88 try:
89 lines = ["%s=%s\n" % (name, getattr(self, name)) for name in self.__slots__ if getattr(self, name)]
90 tmp.write(''.join(lines))
91 tmp.close()
92 os.rename(tmp_name, release_status_file)
93 info("Wrote status to %s", release_status_file)
94 except:
95 os.unlink(tmp_name)
96 raise
98 def get_choice(*options):
99 while True:
100 choice = raw_input('/'.join(options) + ': ').lower()
101 if not choice: continue
102 for o in options:
103 if o.lower().startswith(choice):
104 return o
106 def do_release(local_iface, options):
107 status = Status()
108 local_impl = get_singleton_impl(local_iface)
110 local_impl_dir = local_impl.id
111 assert local_impl_dir.startswith('/')
112 local_impl_dir = os.path.realpath(local_impl_dir)
113 assert os.path.isdir(local_impl_dir)
114 assert local_iface.uri.startswith(local_impl_dir + '/')
115 local_iface_rel_path = local_iface.uri[len(local_impl_dir) + 1:]
116 assert not local_iface_rel_path.startswith('/')
117 assert os.path.isfile(os.path.join(local_impl_dir, local_iface_rel_path))
119 scm = GIT(local_iface)
121 def set_to_release():
122 print "Snapshot version is " + local_impl.get_version()
123 suggested = suggest_release_version(local_impl.get_version())
124 release_version = raw_input("Version number for new release [%s]: " % suggested)
125 if not release_version:
126 release_version = suggested
128 scm.ensure_no_tag(release_version)
130 status.head_before_release = scm.get_head_revision()
131 status.save()
133 print "Releasing version", release_version
134 publish(local_iface.uri, set_released = 'today', set_version = release_version)
136 status.old_snapshot_version = local_impl.get_version()
137 status.release_version = release_version
138 scm.commit('Release %s' % release_version)
139 status.head_at_release = scm.get_head_revision()
140 status.save()
142 return release_version
144 def set_to_snapshot(snapshot_version):
145 assert snapshot_version.endswith('-post')
146 publish(local_iface.uri, set_released = '', set_version = snapshot_version)
147 scm.commit('Start development series %s' % snapshot_version)
148 status.new_snapshot_version = scm.get_head_revision()
149 status.save()
151 def ensure_ready_to_release():
152 scm.ensure_committed()
153 info("No uncommitted changes. Good.")
154 # Not needed for GIT. For SCMs where tagging is expensive (e.g. svn) this might be useful.
155 #run_unit_tests(local_impl)
157 def create_feed(local_iface_stream, archive_file, archive_name, version):
158 tmp = tempfile.NamedTemporaryFile(prefix = '0release-')
159 shutil.copyfileobj(local_iface_stream, tmp)
160 tmp.flush()
162 publish(tmp.name,
163 archive_url = options.archive_dir_public_url + '/' + os.path.basename(archive_file),
164 archive_file = archive_file,
165 archive_extract = archive_name)
166 return tmp
168 def unpack_tarball(archive_file, archive_name):
169 tar = tarfile.open(archive_file, 'r:bz2')
170 members = [m for m in tar.getmembers() if m.name != 'pax_global_header']
171 tar.extractall('.', members = members)
173 def fail_candidate(archive_file):
174 backup_if_exists(archive_file)
175 head = scm.get_head_revision()
176 if head != status.new_snapshot_version:
177 raise SafeException("There have been commits since starting the release! Please rebase them onto %s" % status.head_before_release)
178 # Check no uncommitted changes
179 scm.ensure_committed()
180 scm.reset_hard(status.head_before_release)
181 os.unlink(release_status_file)
182 print "Restored to state before starting release. Make your fixes and try again..."
184 def accept_and_publish(archive_file, archive_name, local_iface_rel_path):
185 assert options.master_feed_file
187 if status.tagged:
188 print "Already tagged and added to master feed."
189 else:
190 tar = tarfile.open(archive_file, 'r:bz2')
191 stream = tar.extractfile(tar.getmember(archive_name + '/' + local_iface_rel_path))
192 remote_dl_iface = create_feed(stream, archive_file, archive_name, version)
193 stream.close()
195 publish(options.master_feed_file, local = remote_dl_iface.name, xmlsign = True, key = options.key)
196 remote_dl_iface.close()
198 scm.tag(status.release_version, status.head_at_release)
200 status.tagged = 'true'
201 status.save()
203 # Copy files...
204 print "Upload %s as %s" % (archive_file, options.archive_dir_public_url + '/' + os.path.basename(archive_file))
205 cmd = options.archive_upload_command.strip()
206 if cmd:
207 show_and_run(cmd, [archive_file])
208 else:
209 print "NOTE: No upload command set => you'll have to upload it yourself!"
211 assert len(local_iface.feed_for) == 1
212 feed_base = os.path.dirname(local_iface.feed_for.keys()[0])
213 feed_files = [options.master_feed_file]
214 print "Upload %s into %s" % (', '.join(feed_files), feed_base)
215 cmd = options.master_feed_upload_command.strip()
216 if cmd:
217 show_and_run(cmd, feed_files)
218 else:
219 print "NOTE: No feed upload command set => you'll have to upload them yourself!"
221 os.unlink(release_status_file)
223 if status.head_before_release:
224 head = scm.get_head_revision()
225 if status.release_version:
226 print "RESUMING release of version %s" % status.release_version
227 elif head == status.head_before_release:
228 print "Restarting release (HEAD revision has not changed)"
229 else:
230 raise SafeException("Something went wrong with the last run:\n" +
231 "HEAD revision for last run was " + status.head_before_release + "\n" +
232 "HEAD revision now is " + head + "\n" +
233 "You should revert your working copy to the previous head and try again.\n" +
234 "If you're sure you want to release from the current head, delete '" + release_status_file + "'")
236 print "Releasing", local_iface.get_name()
238 ensure_ready_to_release()
240 if status.release_version:
241 version = status.release_version
242 need_set_snapshot = False
243 if status.new_snapshot_version:
244 head = scm.get_head_revision()
245 if head != status.new_snapshot_version:
246 print "WARNING: there are more commits since we tagged; they will not be included in the release!"
247 else:
248 raise SafeException("Something went wrong previously when setting the new snapshot version.\n" +
249 "Suggest you reset to the original HEAD of\n%s and delete '%s'." % (status.head_before_release, release_status_file))
250 else:
251 version = set_to_release()
252 need_set_snapshot = True
254 archive_name = local_iface.get_name().lower().replace(' ', '-') + '-' + version
255 archive_file = archive_name + '.tar.bz2'
257 if status.created_archive and os.path.isfile(archive_file):
258 print "Archive already created"
259 else:
260 backup_if_exists(archive_file)
261 scm.export(archive_name, archive_file)
262 status.created_archive = 'true'
263 status.save()
265 if need_set_snapshot:
266 set_to_snapshot(version + '-post')
268 #backup_if_exists(archive_name)
269 unpack_tarball(archive_file, archive_name)
270 if local_impl.main:
271 main = os.path.join(archive_name, local_impl.main)
272 if not os.path.exists(main):
273 raise SafeException("Main executable '%s' not found after unpacking archive!" % main)
275 extracted_iface_path = os.path.abspath(os.path.join(archive_name, local_iface_rel_path))
276 extracted_iface = model.Interface(extracted_iface_path)
277 reader.update(extracted_iface, extracted_iface_path, local = True)
278 extracted_impl = get_singleton_impl(extracted_iface)
279 run_unit_tests(extracted_impl)
281 print "\nCandidate release archive:", archive_file
282 print "(extracted to %s for inspection)" % os.path.abspath(archive_name)
284 print "\nPlease check candidate and select an action:"
285 print "P) Publish candidate (accept)"
286 print "F) Fail candidate (untag)"
287 print "(you can also hit CTRL-C and resume this script when done)"
288 choice = get_choice('Publish', 'Fail')
290 info("Deleting extracted archive %s", archive_name)
291 shutil.rmtree(archive_name)
293 if choice == 'Publish':
294 accept_and_publish(archive_file, archive_name, local_iface_rel_path)
295 else:
296 assert choice == 'Fail'
297 fail_candidate(archive_file)
300 if __name__ == "__main__":
301 import doctest
302 doctest.testmod()