1 # Copyright (C) 2009, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import os
, sys
, subprocess
, shutil
, tempfile
5 from zeroinstall
import SafeException
6 from zeroinstall
.injector
import reader
, model
7 from logging
import info
, warn
9 import support
, compile
10 from scm
import get_scm
12 XMLNS_RELEASE
= 'http://zero-install.sourceforge.net/2007/namespaces/0release'
14 valid_phases
= ['commit-release', 'generate-archive']
16 TMP_BRANCH_NAME
= '0release-tmp'
18 def run_unit_tests(local_feed
, impl
):
19 self_test
= impl
.metadata
.get('self-test', None)
21 print "SKIPPING unit tests for %s (no 'self-test' attribute set)" % impl
23 self_test_dir
= os
.path
.dirname(os
.path
.join(impl
.id, self_test
))
24 print "Running self-test:", self_test
25 exitstatus
= subprocess
.call(['0launch', '--main', self_test
, local_feed
], cwd
= self_test_dir
)
27 raise SafeException("Self-test failed with exit status %d" % exitstatus
)
29 def do_release(local_iface
, options
):
30 assert options
.master_feed_file
31 options
.master_feed_file
= os
.path
.abspath(options
.master_feed_file
)
33 status
= support
.Status()
34 local_impl
= support
.get_singleton_impl(local_iface
)
36 local_impl_dir
= local_impl
.id
37 assert local_impl_dir
.startswith('/')
38 local_impl_dir
= os
.path
.realpath(local_impl_dir
)
39 assert os
.path
.isdir(local_impl_dir
)
40 assert local_iface
.uri
.startswith(local_impl_dir
+ '/')
41 local_iface_rel_path
= local_iface
.uri
[len(local_impl_dir
) + 1:]
42 assert not local_iface_rel_path
.startswith('/')
43 assert os
.path
.isfile(os
.path
.join(local_impl_dir
, local_iface_rel_path
))
46 for phase
in valid_phases
:
47 phase_actions
[phase
] = [] # List of <release:action> elements
49 add_toplevel_dir
= None
50 release_management
= local_iface
.get_metadata(XMLNS_RELEASE
, 'management')
51 if len(release_management
) == 1:
52 info("Found <release:management> element.")
53 release_management
= release_management
[0]
54 for x
in release_management
.childNodes
:
55 if x
.uri
== XMLNS_RELEASE
and x
.name
== 'action':
56 phase
= x
.getAttribute('phase')
57 if phase
not in valid_phases
:
58 raise SafeException("Invalid action phase '%s' in local feed %s. Valid actions are:\n%s" % (phase
, local_iface
.uri
, '\n'.join(valid_phases
)))
59 phase_actions
[phase
].append(x
.content
)
60 elif x
.uri
== XMLNS_RELEASE
and x
.name
== 'add-toplevel-directory':
61 add_toplevel_dir
= local_iface
.get_name()
63 warn("Unknown <release:management> element: %s", x
)
64 elif len(release_management
) > 1:
65 raise SafeException("Multiple <release:management> sections in %s!" % local_iface
)
67 info("No <release:management> element found in local feed.")
69 scm
= get_scm(local_iface
, options
)
71 def run_hooks(phase
, cwd
, env
):
72 info("Running hooks for phase '%s'" % phase
)
73 full_env
= os
.environ
.copy()
75 for x
in phase_actions
[phase
]:
76 print "[%s]: %s" % (phase
, x
)
77 support
.check_call(x
, shell
= True, cwd
= cwd
, env
= full_env
)
80 print "Snapshot version is " + local_impl
.get_version()
81 suggested
= support
.suggest_release_version(local_impl
.get_version())
82 release_version
= raw_input("Version number for new release [%s]: " % suggested
)
83 if not release_version
:
84 release_version
= suggested
86 scm
.ensure_no_tag(release_version
)
88 status
.head_before_release
= scm
.get_head_revision()
91 working_copy
= local_impl
.id
92 run_hooks('commit-release', cwd
= working_copy
, env
= {'RELEASE_VERSION': release_version
})
94 print "Releasing version", release_version
95 support
.publish(local_iface
.uri
, set_released
= 'today', set_version
= release_version
)
97 support
.backup_if_exists(release_version
)
98 os
.mkdir(release_version
)
99 os
.chdir(release_version
)
101 status
.old_snapshot_version
= local_impl
.get_version()
102 status
.release_version
= release_version
103 status
.head_at_release
= scm
.commit('Release %s' % release_version
, branch
= TMP_BRANCH_NAME
, parent
= 'HEAD')
106 def set_to_snapshot(snapshot_version
):
107 assert snapshot_version
.endswith('-post')
108 support
.publish(local_iface
.uri
, set_released
= '', set_version
= snapshot_version
)
109 scm
.commit('Start development series %s' % snapshot_version
, branch
= TMP_BRANCH_NAME
, parent
= TMP_BRANCH_NAME
)
110 status
.new_snapshot_version
= scm
.get_head_revision()
113 def ensure_ready_to_release():
114 if not options
.master_feed_file
:
115 raise SafeException("Master feed file not set! Check your configuration")
117 scm
.ensure_committed()
118 scm
.ensure_versioned(os
.path
.abspath(local_iface
.uri
))
119 info("No uncommitted changes. Good.")
120 # Not needed for GIT. For SCMs where tagging is expensive (e.g. svn) this might be useful.
121 #run_unit_tests(local_impl)
123 scm
.grep('\(^\\|[^=]\)\<\\(TODO\\|XXX\\|FIXME\\)\>')
125 def create_feed(target_feed
, local_iface_path
, archive_file
, archive_name
, main
):
126 shutil
.copyfile(local_iface_path
, target_feed
)
128 support
.publish(target_feed
,
130 archive_url
= options
.archive_dir_public_url
+ '/' + os
.path
.basename(archive_file
),
131 archive_file
= archive_file
,
132 archive_extract
= archive_name
)
134 def get_previous_release(this_version
):
135 """Return the highest numbered verison in the master feed before this_version.
136 @return: version, or None if there wasn't one"""
137 parsed_release_version
= model
.parse_version(this_version
)
139 if os
.path
.exists(options
.master_feed_file
):
140 master
= model
.Interface(os
.path
.realpath(options
.master_feed_file
))
141 reader
.update(master
, master
.uri
, local
= True)
142 versions
= [impl
.version
for impl
in master
.implementations
.values() if impl
.version
< parsed_release_version
]
144 return model
.format_version(max(versions
))
147 def export_changelog(previous_release
):
148 changelog
= file('changelog-%s' % status
.release_version
, 'w')
151 scm
.export_changelog(previous_release
, status
.head_before_release
, changelog
)
152 except SafeException
, ex
:
153 print "WARNING: Failed to generate changelog: " + str(ex
)
155 print "Wrote changelog from %s to here as %s" % (previous_release
or 'start', changelog
.name
)
159 def fail_candidate(archive_file
):
161 assert cwd
.endswith(status
.release_version
)
162 support
.backup_if_exists(cwd
)
163 scm
.delete_branch(TMP_BRANCH_NAME
)
164 os
.unlink(support
.release_status_file
)
165 print "Restored to state before starting release. Make your fixes and try again..."
167 def accept_and_publish(archive_file
, archive_name
, src_feed_name
):
168 assert options
.master_feed_file
170 if not options
.archive_dir_public_url
:
171 raise SafeException("Archive directory public URL is not set! Edit configuration and try again.")
174 print "Already tagged in SCM. Not re-tagging."
176 scm
.ensure_committed()
177 head
= scm
.get_head_revision()
178 if head
!= status
.head_before_release
:
179 raise SafeException("Changes committed since we started!\n" +
180 "HEAD was " + status
.head_before_release
+ "\n"
183 scm
.tag(status
.release_version
, status
.head_at_release
)
184 scm
.reset_hard(TMP_BRANCH_NAME
)
185 scm
.delete_branch(TMP_BRANCH_NAME
)
187 status
.tagged
= 'true'
190 if status
.updated_master_feed
:
191 print "Already added to master feed. Not changing."
193 if os
.path
.exists(options
.master_feed_file
):
194 # Check we haven't already released this version
195 master
= model
.Interface(os
.path
.realpath(options
.master_feed_file
))
196 reader
.update(master
, master
.uri
, local
= True)
197 existing_releases
= [impl
for impl
in master
.implementations
.values() if impl
.get_version() == status
.release_version
]
198 if len(existing_releases
):
199 raise SafeException("Master feed %s already contains an implementation with version number %s!" % (options
.master_feed_file
, status
.release_version
))
201 # Merge the source and binary feeds together first, so
202 # that we update the master feed atomically and only
203 # have to sign it once.
204 shutil
.copyfile(src_feed_name
, 'merged.xml')
205 for b
in compiler
.get_binary_feeds():
206 support
.publish('merged.xml', local
= b
)
208 support
.publish(options
.master_feed_file
, local
= 'merged.xml', xmlsign
= True, key
= options
.key
)
209 os
.unlink('merged.xml')
211 status
.updated_master_feed
= 'true'
214 def is_uploaded(url
, size
):
215 if url
.startswith('http://TESTING/releases'):
218 print "Testing URL %s..." % url
220 actual_size
= int(support
.get_size(url
))
221 except Exception, ex
:
222 print "Can't get size of '%s': %s" % (url
, ex
)
225 if actual_size
== size
:
227 print "WARNING: %s exists, but size is %d, not %d!" % (url
, actual_size
, size
)
232 archive_url
= options
.archive_dir_public_url
+ '/' + os
.path
.basename(archive_file
)
233 archive_size
= os
.path
.getsize(archive_file
)
234 if status
.uploaded_archive
and is_uploaded(archive_url
, archive_size
):
235 print "Archive already uploaded. Not uploading again."
238 print "Upload %s/%s as %s" % (status
.release_version
, archive_file
, archive_url
)
239 cmd
= options
.archive_upload_command
.strip()
241 support
.show_and_run(cmd
, [archive_file
])
243 print "No upload command is set => please upload the archive manually now"
244 raw_input('Press Return once archive is uploaded.')
246 if is_uploaded(archive_url
, archive_size
):
247 print "OK, archive uploaded successfully"
248 status
.uploaded_archive
= 'true'
251 print "** Archive still not uploaded! Try again..."
253 raw_input('Press Return to retry upload command.')
255 assert len(local_iface
.feed_for
) == 1
256 feed_base
= os
.path
.dirname(local_iface
.feed_for
.keys()[0])
257 feed_files
= [options
.master_feed_file
]
258 print "Upload %s into %s" % (', '.join(feed_files
), feed_base
)
259 cmd
= options
.master_feed_upload_command
.strip()
261 support
.show_and_run(cmd
, feed_files
)
263 print "NOTE: No feed upload command set => you'll have to upload them yourself!"
265 print "Push changes to public SCM repository..."
266 public_repos
= options
.public_scm_repository
268 scm
.push_head_and_release(status
.release_version
)
270 print "NOTE: No public repository set => you'll have to push the tag and trunk yourself."
272 os
.unlink(support
.release_status_file
)
274 if status
.head_before_release
:
275 head
= scm
.get_head_revision()
276 if status
.release_version
:
277 print "RESUMING release of %s %s" % (local_iface
.get_name(), status
.release_version
)
278 elif head
== status
.head_before_release
:
279 print "Restarting release of %s (HEAD revision has not changed)" % local_iface
.get_name()
281 raise SafeException("Something went wrong with the last run:\n" +
282 "HEAD revision for last run was " + status
.head_before_release
+ "\n" +
283 "HEAD revision now is " + head
+ "\n" +
284 "You should revert your working copy to the previous head and try again.\n" +
285 "If you're sure you want to release from the current head, delete '" + support
.release_status_file
+ "'")
287 print "Releasing", local_iface
.get_name()
289 ensure_ready_to_release()
291 if status
.release_version
:
292 if not os
.path
.isdir(status
.release_version
):
293 raise SafeException("Can't resume; directory %s missing. Try deleting '%s'." % (status
.release_version
, support
.release_status_file
))
294 os
.chdir(status
.release_version
)
295 need_set_snapshot
= False
297 print "Already tagged. Resuming the publishing process..."
298 elif status
.new_snapshot_version
:
299 head
= scm
.get_head_revision()
300 if head
!= status
.head_before_release
:
301 raise SafeException("There are more commits since we started!\n"
302 "HEAD was " + status
.head_before_release
+ "\n"
303 "HEAD now " + head
+ "\n"
304 "To include them, delete '" + support
.release_status_file
+ "' and try again.\n"
305 "To leave them out, put them on a new branch and reset HEAD to the release version.")
307 raise SafeException("Something went wrong previously when setting the new snapshot version.\n" +
308 "Suggest you reset to the original HEAD of\n%s and delete '%s'." % (status
.head_before_release
, support
.release_status_file
))
310 set_to_release() # Changes directory
311 assert status
.release_version
312 need_set_snapshot
= True
314 archive_name
= support
.make_archive_name(local_iface
.get_name(), status
.release_version
)
315 archive_file
= archive_name
+ '.tar.bz2'
317 export_prefix
= archive_name
318 if add_toplevel_dir
is not None:
319 export_prefix
+= '/' + add_toplevel_dir
321 if status
.created_archive
and os
.path
.isfile(archive_file
):
322 print "Archive already created"
324 support
.backup_if_exists(archive_file
)
325 scm
.export(export_prefix
, archive_file
, status
.head_at_release
)
327 has_submodules
= scm
.has_submodules()
329 if phase_actions
['generate-archive'] or has_submodules
:
331 support
.unpack_tarball(archive_file
)
333 scm
.export_submodules(archive_name
)
334 run_hooks('generate-archive', cwd
= archive_name
, env
= {'RELEASE_VERSION': status
.release_version
})
335 info("Regenerating archive (may have been modified by generate-archive hooks...")
336 support
.check_call(['tar', 'cjf', archive_file
, archive_name
])
337 except SafeException
:
338 scm
.reset_hard(scm
.get_current_branch())
339 fail_candidate(archive_file
)
342 status
.created_archive
= 'true'
345 if need_set_snapshot
:
346 set_to_snapshot(status
.release_version
+ '-post')
347 # Revert back to the original revision, so that any fixes the user makes
348 # will get applied before the tag
349 scm
.reset_hard(scm
.get_current_branch())
351 #backup_if_exists(archive_name)
352 support
.unpack_tarball(archive_file
)
354 main
= os
.path
.join(export_prefix
, local_impl
.main
)
355 if not os
.path
.exists(main
):
356 raise SafeException("Main executable '%s' not found after unpacking archive!" % main
)
358 extracted_iface_path
= os
.path
.abspath(os
.path
.join(export_prefix
, local_iface_rel_path
))
359 assert os
.path
.isfile(extracted_iface_path
), "Local feed not in archive! Is it under version control?"
360 extracted_iface
= model
.Interface(extracted_iface_path
)
361 reader
.update(extracted_iface
, extracted_iface_path
, local
= True)
362 extracted_impl
= support
.get_singleton_impl(extracted_iface
)
365 run_unit_tests(extracted_iface_path
, extracted_impl
)
366 except SafeException
:
367 print "(leaving extracted directory for examination)"
368 fail_candidate(archive_file
)
370 # Unpack it again in case the unit-tests changed anything
371 shutil
.rmtree(archive_name
)
372 support
.unpack_tarball(archive_file
)
374 # Generate feed for source
375 stream
= open(extracted_iface_path
)
376 main
= extracted_impl
.main
377 if main
and add_toplevel_dir
:
378 main
= os
.path
.join(add_toplevel_dir
, main
)
379 src_feed_name
= '%s.xml' % archive_name
380 create_feed(src_feed_name
, extracted_iface_path
, archive_file
, archive_name
, main
)
381 print "Wrote source feed as %s" % src_feed_name
383 # If it's a source package, compile the binaries now...
384 compiler
= compile.Compiler(options
, os
.path
.abspath(src_feed_name
))
385 compiler
.build_binaries()
387 previous_release
= get_previous_release(status
.release_version
)
388 export_changelog(previous_release
)
390 print "\nCandidate release archive:", archive_file
391 print "(extracted to %s for inspection)" % os
.path
.abspath(archive_name
)
393 print "\nPlease check candidate and select an action:"
394 print "P) Publish candidate (accept)"
395 print "F) Fail candidate (untag)"
397 print "D) Diff against release archive for %s" % previous_release
398 maybe_diff
= ['Diff']
401 print "(you can also hit CTRL-C and resume this script when done)"
404 choice
= support
.get_choice(['Publish', 'Fail'] + maybe_diff
)
406 previous_archive_name
= support
.make_archive_name(local_iface
.get_name(), previous_release
)
407 previous_archive_file
= '../%s/%s.tar.bz2' % (previous_release
, previous_archive_name
)
409 # For archives created by older versions of 0release
410 if not os
.path
.isfile(previous_archive_file
):
411 old_previous_archive_file
= '../%s.tar.bz2' % previous_archive_name
412 if os
.path
.isfile(old_previous_archive_file
):
413 previous_archive_file
= old_previous_archive_file
415 if os
.path
.isfile(previous_archive_file
):
416 support
.unpack_tarball(previous_archive_file
)
418 support
.show_diff(previous_archive_name
, archive_name
)
420 shutil
.rmtree(previous_archive_name
)
423 print "Sorry, archive file %s not found! Can't show diff." % previous_archive_file
427 info("Deleting extracted archive %s", archive_name
)
428 shutil
.rmtree(archive_name
)
430 if choice
== 'Publish':
431 accept_and_publish(archive_file
, archive_name
, src_feed_name
)
433 assert choice
== 'Fail'
434 fail_candidate(archive_file
)