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
10 release_status_file
= 'release-status'
12 def run_unit_tests(impl
):
13 self_test
= impl
.metadata
.get('self-test', None)
15 print "SKIPPING unit tests for %s (no 'self-test' attribute set)" % impl
17 self_test
= os
.path
.join(impl
.id, self_test
)
18 print "Running self-test:", self_test
19 exitstatus
= subprocess
.call([self_test
], cwd
= impl
.id)
21 raise SafeException("Self-test failed with exit status %d" % exitstatus
)
23 def show_and_run(cmd
, args
):
24 print "Executing: %s %s" % (cmd
, ' '.join("[%s]" % x
for x
in args
))
25 subprocess
.check_call(['sh', '-c', cmd
, '-'] + args
)
27 def suggest_release_version(snapshot_version
):
28 """Given a snapshot version, suggest a suitable release version.
29 >>> suggest_release_version('1.0-pre')
31 >>> suggest_release_version('0.9-post')
33 >>> suggest_release_version('3')
34 Traceback (most recent call last):
36 SafeException: Version '3' is not a snapshot version (should end in -pre or -post)
38 version
= model
.parse_version(snapshot_version
)
41 raise SafeException("Version '%s' is not a snapshot version (should end in -pre or -post)" % snapshot_version
)
43 # -post, so increment the number
45 version
[-1] = 0 # Remove the modifier
46 return model
.format_version(version
)
48 def publish(iface
, **kwargs
):
49 args
= [os
.environ
['0PUBLISH']]
53 args
+= ['--' + k
.replace('_', '-')]
54 elif value
is not None:
55 args
+= ['--' + k
.replace('_', '-'), value
]
57 info("Executing %s", args
)
58 subprocess
.check_call(args
)
60 def get_singleton_impl(iface
):
61 impls
= iface
.implementations
63 raise SafeException("Local feed '%s' contains %d versions! I need exactly one!" % (iface
.uri
, len(impls
)))
64 return impls
.values()[0]
66 def backup_if_exists(name
):
67 if not os
.path
.exists(name
):
70 if os
.path
.exists(backup
):
71 print "(deleting old backup %s)" % backup
72 if os
.path
.isdir(backup
):
76 os
.rename(name
, backup
)
77 print "(renamed old %s as %s; will delete on next run)" % (name
, backup
)
80 __slots__
= ['old_snapshot_version', 'release_version', 'head_before_release', 'new_snapshot_version', 'head_at_release', 'created_archive', 'tagged']
82 for name
in self
.__slots
__:
83 setattr(self
, name
, None)
85 if os
.path
.isfile(release_status_file
):
86 for line
in file(release_status_file
):
87 assert line
.endswith('\n')
89 name
, value
= line
.split('=')
90 setattr(self
, name
, value
)
91 info("Loaded status %s=%s", name
, value
)
94 tmp_name
= release_status_file
+ '.new'
95 tmp
= file(tmp_name
, 'w')
97 lines
= ["%s=%s\n" % (name
, getattr(self
, name
)) for name
in self
.__slots
__ if getattr(self
, name
)]
98 tmp
.write(''.join(lines
))
100 os
.rename(tmp_name
, release_status_file
)
101 info("Wrote status to %s", release_status_file
)
106 def get_choice(*options
):
108 choice
= raw_input('/'.join(options
) + ': ').lower()
109 if not choice
: continue
111 if o
.lower().startswith(choice
):
114 def do_release(local_iface
, options
):
116 local_impl
= get_singleton_impl(local_iface
)
118 local_impl_dir
= local_impl
.id
119 assert local_impl_dir
.startswith('/')
120 local_impl_dir
= os
.path
.realpath(local_impl_dir
)
121 assert os
.path
.isdir(local_impl_dir
)
122 assert local_iface
.uri
.startswith(local_impl_dir
+ '/')
123 local_iface_rel_path
= local_iface
.uri
[len(local_impl_dir
) + 1:]
124 assert not local_iface_rel_path
.startswith('/')
125 assert os
.path
.isfile(os
.path
.join(local_impl_dir
, local_iface_rel_path
))
127 scm
= GIT(local_iface
)
129 def set_to_release():
130 print "Snapshot version is " + local_impl
.get_version()
131 suggested
= suggest_release_version(local_impl
.get_version())
132 release_version
= raw_input("Version number for new release [%s]: " % suggested
)
133 if not release_version
:
134 release_version
= suggested
136 scm
.ensure_no_tag(release_version
)
138 status
.head_before_release
= scm
.get_head_revision()
141 print "Releasing version", release_version
142 publish(local_iface
.uri
, set_released
= 'today', set_version
= release_version
)
144 status
.old_snapshot_version
= local_impl
.get_version()
145 status
.release_version
= release_version
146 scm
.commit('Release %s' % release_version
)
147 status
.head_at_release
= scm
.get_head_revision()
150 return release_version
152 def set_to_snapshot(snapshot_version
):
153 assert snapshot_version
.endswith('-post')
154 publish(local_iface
.uri
, set_released
= '', set_version
= snapshot_version
)
155 scm
.commit('Start development series %s' % snapshot_version
)
156 status
.new_snapshot_version
= scm
.get_head_revision()
159 def ensure_ready_to_release():
160 scm
.ensure_committed()
161 info("No uncommitted changes. Good.")
162 # Not needed for GIT. For SCMs where tagging is expensive (e.g. svn) this might be useful.
163 #run_unit_tests(local_impl)
165 def create_feed(local_iface_stream
, archive_file
, archive_name
, version
):
166 tmp
= tempfile
.NamedTemporaryFile(prefix
= '0release-')
167 shutil
.copyfileobj(local_iface_stream
, tmp
)
171 archive_url
= options
.archive_dir_public_url
+ '/' + os
.path
.basename(archive_file
),
172 archive_file
= archive_file
,
173 archive_extract
= archive_name
)
176 def unpack_tarball(archive_file
, archive_name
):
177 tar
= tarfile
.open(archive_file
, 'r:bz2')
178 members
= [m
for m
in tar
.getmembers() if m
.name
!= 'pax_global_header']
179 tar
.extractall('.', members
= members
)
181 def fail_candidate(archive_file
):
182 backup_if_exists(archive_file
)
183 head
= scm
.get_head_revision()
184 if head
!= status
.new_snapshot_version
:
185 raise SafeException("There have been commits since starting the release! Please rebase them onto %s" % status
.head_before_release
)
186 # Check no uncommitted changes
187 scm
.ensure_committed()
188 scm
.reset_hard(status
.head_before_release
)
189 os
.unlink(release_status_file
)
190 print "Restored to state before starting release. Make your fixes and try again..."
192 def accept_and_publish(archive_file
, archive_name
, local_iface_rel_path
):
193 assert options
.master_feed_file
196 print "Already tagged and added to master feed."
198 tar
= tarfile
.open(archive_file
, 'r:bz2')
199 stream
= tar
.extractfile(tar
.getmember(archive_name
+ '/' + local_iface_rel_path
))
200 remote_dl_iface
= create_feed(stream
, archive_file
, archive_name
, version
)
203 publish(options
.master_feed_file
, local
= remote_dl_iface
.name
, xmlsign
= True, key
= options
.key
)
204 remote_dl_iface
.close()
206 scm
.tag(status
.release_version
, status
.head_at_release
)
208 status
.tagged
= 'true'
212 print "Upload %s as %s" % (archive_file
, options
.archive_dir_public_url
+ '/' + os
.path
.basename(archive_file
))
213 cmd
= options
.archive_upload_command
.strip()
215 show_and_run(cmd
, [archive_file
])
217 print "NOTE: No upload command set => you'll have to upload it yourself!"
219 assert len(local_iface
.feed_for
) == 1
220 feed_base
= os
.path
.dirname(local_iface
.feed_for
.keys()[0])
221 feed_files
= [options
.master_feed_file
]
222 print "Upload %s into %s" % (', '.join(feed_files
), feed_base
)
223 cmd
= options
.master_feed_upload_command
.strip()
225 show_and_run(cmd
, feed_files
)
227 print "NOTE: No feed upload command set => you'll have to upload them yourself!"
229 os
.unlink(release_status_file
)
231 if status
.head_before_release
:
232 head
= scm
.get_head_revision()
233 if status
.release_version
:
234 print "RESUMING release of version %s" % status
.release_version
235 elif head
== status
.head_before_release
:
236 print "Restarting release (HEAD revision has not changed)"
238 raise SafeException("Something went wrong with the last run:\n" +
239 "HEAD revision for last run was " + status
.head_before_release
+ "\n" +
240 "HEAD revision now is " + head
+ "\n" +
241 "You should revert your working copy to the previous head and try again.\n" +
242 "If you're sure you want to release from the current head, delete '" + release_status_file
+ "'")
244 print "Releasing", local_iface
.get_name()
246 ensure_ready_to_release()
248 if status
.release_version
:
249 version
= status
.release_version
250 need_set_snapshot
= False
251 if status
.new_snapshot_version
:
252 head
= scm
.get_head_revision()
253 if head
!= status
.new_snapshot_version
:
254 print "WARNING: there are more commits since we tagged; they will not be included in the release!"
256 raise SafeException("Something went wrong previously when setting the new snapshot version.\n" +
257 "Suggest you reset to the original HEAD of\n%s and delete '%s'." % (status
.head_before_release
, release_status_file
))
259 version
= set_to_release()
260 need_set_snapshot
= True
262 archive_name
= local_iface
.get_name().lower().replace(' ', '-') + '-' + version
263 archive_file
= archive_name
+ '.tar.bz2'
265 if status
.created_archive
and os
.path
.isfile(archive_file
):
266 print "Archive already created"
268 backup_if_exists(archive_file
)
269 scm
.export(archive_name
, archive_file
)
270 status
.created_archive
= 'true'
273 if need_set_snapshot
:
274 set_to_snapshot(version
+ '-post')
276 #backup_if_exists(archive_name)
277 unpack_tarball(archive_file
, archive_name
)
279 main
= os
.path
.join(archive_name
, local_impl
.main
)
280 if not os
.path
.exists(main
):
281 raise SafeException("Main executable '%s' not found after unpacking archive!" % main
)
283 extracted_iface_path
= os
.path
.abspath(os
.path
.join(archive_name
, local_iface_rel_path
))
284 extracted_iface
= model
.Interface(extracted_iface_path
)
285 reader
.update(extracted_iface
, extracted_iface_path
, local
= True)
286 extracted_impl
= get_singleton_impl(extracted_iface
)
289 run_unit_tests(extracted_impl
)
290 except SafeException
:
291 print "(leaving extracted directory for examination)"
292 fail_candidate(archive_file
)
295 print "\nCandidate release archive:", archive_file
296 print "(extracted to %s for inspection)" % os
.path
.abspath(archive_name
)
298 print "\nPlease check candidate and select an action:"
299 print "P) Publish candidate (accept)"
300 print "F) Fail candidate (untag)"
301 print "(you can also hit CTRL-C and resume this script when done)"
302 choice
= get_choice('Publish', 'Fail')
304 info("Deleting extracted archive %s", archive_name
)
305 shutil
.rmtree(archive_name
)
307 if choice
== 'Publish':
308 accept_and_publish(archive_file
, archive_name
, local_iface_rel_path
)
310 assert choice
== 'Fail'
311 fail_candidate(archive_file
)
314 if __name__
== "__main__":