blob: 80a2edb5b3eb359f2a13cb508f8441374290d561 [file] [log] [blame]
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +00001"""Tag the sandbox for release, make source and doc tarballs.
2
3Requires Python 2.6
4
5Example of invocation (use to test the script):
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +00006python makerelease.py --force --retag --platform=msvc6,msvc71,msvc80,mingw -ublep 0.5.0 0.6.0-dev
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +00007
8Example of invocation when doing a release:
9python makerelease.py 0.5.0 0.6.0-dev
10"""
11import os.path
12import subprocess
13import sys
14import doxybuild
15import subprocess
16import xml.etree.ElementTree as ElementTree
Baptiste Lepilleur7c171ee2010-02-23 08:44:52 +000017import shutil
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +000018import urllib2
19import tempfile
20import os
21import time
Baptiste Lepilleure94d2f42010-02-23 21:00:30 +000022from devtools import antglob, fixeol, tarball
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +000023
24SVN_ROOT = 'https://jsoncpp.svn.sourceforge.net/svnroot/jsoncpp/'
25SVN_TAG_ROOT = SVN_ROOT + 'tags/jsoncpp'
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +000026SCONS_LOCAL_URL = 'http://sourceforge.net/projects/scons/files/scons-local/1.2.0/scons-local-1.2.0.tar.gz/download'
27SOURCEFORGE_PROJECT = 'jsoncpp'
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +000028
29def set_version( version ):
30 with open('version','wb') as f:
31 f.write( version.strip() )
32
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +000033def rmdir_if_exist( dir_path ):
34 if os.path.isdir( dir_path ):
35 shutil.rmtree( dir_path )
36
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +000037class SVNError(Exception):
38 pass
39
40def svn_command( command, *args ):
41 cmd = ['svn', '--non-interactive', command] + list(args)
42 print 'Running:', ' '.join( cmd )
43 process = subprocess.Popen( cmd,
44 stdout=subprocess.PIPE,
45 stderr=subprocess.STDOUT )
46 stdout = process.communicate()[0]
47 if process.returncode:
48 error = SVNError( 'SVN command failed:\n' + stdout )
49 error.returncode = process.returncode
50 raise error
51 return stdout
52
53def check_no_pending_commit():
54 """Checks that there is no pending commit in the sandbox."""
55 stdout = svn_command( 'status', '--xml' )
56 etree = ElementTree.fromstring( stdout )
57 msg = []
58 for entry in etree.getiterator( 'entry' ):
59 path = entry.get('path')
60 status = entry.find('wc-status').get('item')
61 if status != 'unversioned':
62 msg.append( 'File "%s" has pending change (status="%s")' % (path, status) )
63 if msg:
64 msg.insert(0, 'Pending change to commit found in sandbox. Commit them first!' )
65 return '\n'.join( msg )
66
67def svn_join_url( base_url, suffix ):
68 if not base_url.endswith('/'):
69 base_url += '/'
70 if suffix.startswith('/'):
71 suffix = suffix[1:]
72 return base_url + suffix
73
74def svn_check_if_tag_exist( tag_url ):
75 """Checks if a tag exist.
76 Returns: True if the tag exist, False otherwise.
77 """
78 try:
79 list_stdout = svn_command( 'list', tag_url )
80 except SVNError, e:
81 if e.returncode != 1 or not str(e).find('tag_url'):
82 raise e
83 # otherwise ignore error, meaning tag does not exist
84 return False
85 return True
86
87def svn_tag_sandbox( tag_url, message ):
88 """Makes a tag based on the sandbox revisions.
89 """
90 svn_command( 'copy', '-m', message, '.', tag_url )
91
92def svn_remove_tag( tag_url, message ):
93 """Removes an existing tag.
94 """
95 svn_command( 'delete', '-m', message, tag_url )
96
Baptiste Lepilleur7c171ee2010-02-23 08:44:52 +000097def svn_export( tag_url, export_dir ):
98 """Exports the tag_url revision to export_dir.
99 Target directory, including its parent is created if it does not exist.
100 If the directory export_dir exist, it is deleted before export proceed.
101 """
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000102 rmdir_if_exist( export_dir )
Baptiste Lepilleur7c171ee2010-02-23 08:44:52 +0000103 svn_command( 'export', tag_url, export_dir )
104
Baptiste Lepilleure94d2f42010-02-23 21:00:30 +0000105def fix_sources_eol( dist_dir ):
106 """Set file EOL for tarball distribution.
107 """
108 print 'Preparing exported source file EOL for distribution...'
109 prune_dirs = antglob.prune_dirs + 'scons-local* ./build* ./libs ./dist'
110 win_sources = antglob.glob( dist_dir,
111 includes = '**/*.sln **/*.vcproj',
112 prune_dirs = prune_dirs )
113 unix_sources = antglob.glob( dist_dir,
114 includes = '''**/*.h **/*.cpp **/*.inl **/*.txt **/*.dox **/*.py **/*.html **/*.in
115 sconscript *.json *.expected AUTHORS LICENSE''',
116 excludes = antglob.default_excludes + 'scons.py sconsign.py scons-*',
117 prune_dirs = prune_dirs )
118 for path in win_sources:
119 fixeol.fix_source_eol( path, is_dry_run = False, verbose = True, eol = '\r\n' )
120 for path in unix_sources:
121 fixeol.fix_source_eol( path, is_dry_run = False, verbose = True, eol = '\n' )
122
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000123def download( url, target_path ):
124 """Download file represented by url to target_path.
125 """
126 f = urllib2.urlopen( url )
127 try:
128 data = f.read()
129 finally:
130 f.close()
131 fout = open( target_path, 'wb' )
132 try:
133 fout.write( data )
134 finally:
135 fout.close()
136
137def check_compile( distcheck_top_dir, platform ):
138 cmd = [sys.executable, 'scons.py', 'platform=%s' % platform, 'check']
139 print 'Running:', ' '.join( cmd )
140 log_path = os.path.join( distcheck_top_dir, 'build-%s.log' % platform )
141 flog = open( log_path, 'wb' )
142 try:
143 process = subprocess.Popen( cmd,
144 stdout=flog,
145 stderr=subprocess.STDOUT,
146 cwd=distcheck_top_dir )
147 stdout = process.communicate()[0]
148 status = (process.returncode == 0)
149 finally:
150 flog.close()
151 return (status, log_path)
152
153def write_tempfile( content, **kwargs ):
154 fd, path = tempfile.mkstemp( **kwargs )
155 f = os.fdopen( fd, 'wt' )
156 try:
157 f.write( content )
158 finally:
159 f.close()
160 return path
161
162class SFTPError(Exception):
163 pass
164
165def run_sftp_batch( userhost, sftp, batch, retry=0 ):
166 path = write_tempfile( batch, suffix='.sftp', text=True )
167 # psftp -agent -C blep,jsoncpp@web.sourceforge.net -batch -b batch.sftp -bc
168 cmd = [sftp, '-agent', '-C', '-batch', '-b', path, '-bc', userhost]
169 error = None
170 for retry_index in xrange(0, max(1,retry)):
171 heading = retry_index == 0 and 'Running:' or 'Retrying:'
172 print heading, ' '.join( cmd )
173 process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT )
174 stdout = process.communicate()[0]
175 if process.returncode != 0:
176 error = SFTPError( 'SFTP batch failed:\n' + stdout )
177 else:
178 break
179 if error:
180 raise error
181 return stdout
182
183def sourceforge_web_synchro( sourceforge_project, doc_dir,
184 user=None, sftp='sftp' ):
185 """Notes: does not synchronize sub-directory of doc-dir.
186 """
187 userhost = '%s,%s@web.sourceforge.net' % (user, sourceforge_project)
188 stdout = run_sftp_batch( userhost, sftp, """
189cd htdocs
190dir
191exit
192""" )
193 existing_paths = set()
194 collect = 0
195 for line in stdout.split('\n'):
196 line = line.strip()
197 if not collect and line.endswith('> dir'):
198 collect = True
199 elif collect and line.endswith('> exit'):
200 break
201 elif collect == 1:
202 collect = 2
203 elif collect == 2:
204 path = line.strip().split()[-1:]
205 if path and path[0] not in ('.', '..'):
206 existing_paths.add( path[0] )
207 upload_paths = set( [os.path.basename(p) for p in antglob.glob( doc_dir )] )
208 paths_to_remove = existing_paths - upload_paths
209 if paths_to_remove:
210 print 'Removing the following file from web:'
211 print '\n'.join( paths_to_remove )
212 stdout = run_sftp_batch( userhost, sftp, """cd htdocs
213rm %s
214exit""" % ' '.join(paths_to_remove) )
215 print 'Uploading %d files:' % len(upload_paths)
216 batch_size = 10
217 upload_paths = list(upload_paths)
218 start_time = time.time()
219 for index in xrange(0,len(upload_paths),batch_size):
220 paths = upload_paths[index:index+batch_size]
221 file_per_sec = (time.time() - start_time) / (index+1)
222 remaining_files = len(upload_paths) - index
223 remaining_sec = file_per_sec * remaining_files
224 print '%d/%d, ETA=%.1fs' % (index+1, len(upload_paths), remaining_sec)
225 run_sftp_batch( userhost, sftp, """cd htdocs
226lcd %s
227mput %s
228exit""" % (doc_dir, ' '.join(paths) ), retry=3 )
229
230
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +0000231def main():
232 usage = """%prog release_version next_dev_version
233Update 'version' file to release_version and commit.
234Generates the document tarball.
235Tags the sandbox revision with release_version.
236Update 'version' file to next_dev_version and commit.
237
238Performs an svn export of tag release version, and build a source tarball.
239
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000240Must be started in the project top directory.
241
242Warning: --force should only be used when developping/testing the release script.
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +0000243"""
244 from optparse import OptionParser
245 parser = OptionParser(usage=usage)
246 parser.allow_interspersed_args = False
247 parser.add_option('--dot', dest="dot_path", action='store', default=doxybuild.find_program('dot'),
248 help="""Path to GraphViz dot tool. Must be full qualified path. [Default: %default]""")
249 parser.add_option('--doxygen', dest="doxygen_path", action='store', default=doxybuild.find_program('doxygen'),
250 help="""Path to Doxygen tool. [Default: %default]""")
251 parser.add_option('--force', dest="ignore_pending_commit", action='store_true', default=False,
252 help="""Ignore pending commit. [Default: %default]""")
253 parser.add_option('--retag', dest="retag_release", action='store_true', default=False,
254 help="""Overwrite release existing tag if it exist. [Default: %default]""")
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000255 parser.add_option('-p', '--platforms', dest="platforms", action='store', default='',
256 help="""Comma separated list of platform passed to scons for build check.""")
257 parser.add_option('--no-test', dest="no_test", action='store', default=False,
258 help="""Skips build check.""")
259 parser.add_option('-u', '--upload-user', dest="user", action='store',
260 help="""Sourceforge user for SFTP documentation upload.""")
261 parser.add_option('--sftp', dest='sftp', action='store', default=doxybuild.find_program('psftp', 'sftp'),
262 help="""Path of the SFTP compatible binary used to upload the documentation.""")
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +0000263 parser.enable_interspersed_args()
264 options, args = parser.parse_args()
265
266 if len(args) < 1:
267 parser.error( 'release_version missing on command-line.' )
268 release_version = args[0]
269
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000270 if not options.platforms and not options.no_test:
271 parser.error( 'You must specify either --platform or --no-test option.' )
272
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +0000273 if options.ignore_pending_commit:
274 msg = ''
275 else:
276 msg = check_no_pending_commit()
277 if not msg:
278 print 'Setting version to', release_version
279 set_version( release_version )
280 tag_url = svn_join_url( SVN_TAG_ROOT, release_version )
Baptiste Lepilleure94d2f42010-02-23 21:00:30 +0000281 if svn_check_if_tag_exist( tag_url ):
282 if options.retag_release:
283 svn_remove_tag( tag_url, 'Overwriting previous tag' )
284 else:
285 print 'Aborting, tag %s already exist. Use --retag to overwrite it!' % tag_url
286 sys.exit( 1 )
287 svn_tag_sandbox( tag_url, 'Release ' + release_version )
288
289 print 'Generated doxygen document...'
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000290## doc_dirname = r'jsoncpp-api-html-0.5.0'
291## doc_tarball_path = r'e:\prg\vc\Lib\jsoncpp-trunk\dist\jsoncpp-api-html-0.5.0.tar.gz'
292 doc_tarball_path, doc_dirname = doxybuild.build_doc( options, make_release=True )
293 doc_distcheck_dir = 'dist/doccheck'
294 tarball.decompress( doc_tarball_path, doc_distcheck_dir )
295 doc_distcheck_top_dir = os.path.join( doc_distcheck_dir, doc_dirname )
Baptiste Lepilleure94d2f42010-02-23 21:00:30 +0000296
297 export_dir = 'dist/export'
298 svn_export( tag_url, export_dir )
299 fix_sources_eol( export_dir )
300
301 source_dir = 'jsoncpp-src-' + release_version
302 source_tarball_path = 'dist/%s.tar.gz' % source_dir
303 print 'Generating source tarball to', source_tarball_path
304 tarball.make_tarball( source_tarball_path, [export_dir], export_dir, prefix_dir=source_dir )
Baptiste Lepilleur35bdc072010-02-24 08:05:41 +0000305
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000306 # Decompress source tarball, download and install scons-local
Baptiste Lepilleur35bdc072010-02-24 08:05:41 +0000307 distcheck_dir = 'dist/distcheck'
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000308 distcheck_top_dir = distcheck_dir + '/' + source_dir
Baptiste Lepilleur35bdc072010-02-24 08:05:41 +0000309 print 'Decompressing source tarball to', distcheck_dir
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000310 rmdir_if_exist( distcheck_dir )
Baptiste Lepilleur35bdc072010-02-24 08:05:41 +0000311 tarball.decompress( source_tarball_path, distcheck_dir )
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000312 scons_local_path = 'dist/scons-local.tar.gz'
313 print 'Downloading scons-local to', scons_local_path
314 download( SCONS_LOCAL_URL, scons_local_path )
315 print 'Decompressing scons-local to', distcheck_top_dir
316 tarball.decompress( scons_local_path, distcheck_top_dir )
317
318 # Run compilation
319 print 'Compiling decompressed tarball'
320 all_build_status = True
321 for platform in options.platforms.split(','):
322 print 'Testing platform:', platform
323 build_status, log_path = check_compile( distcheck_top_dir, platform )
324 print 'see build log:', log_path
325 print build_status and '=> ok' or '=> FAILED'
326 all_build_status = all_build_status and build_status
327 if not build_status:
328 print 'Testing failed on at least one platform, aborting...'
329 svn_remove_tag( tag_url, 'Removing tag due to failed testing' )
330 sys.exit(1)
331 if options.user:
332 print 'Uploading documentation using user', options.user
333 sourceforge_web_synchro( SOURCEFORGE_PROJECT, doc_distcheck_top_dir, user=options.user, sftp=options.sftp )
334 print 'Completed documentatio upload'
335 else:
336 print 'No upload user specified. Documentation was not upload.'
337 print 'Tarball can be found at:', doc_tarball_path
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +0000338 #@todo:
Baptiste Lepilleur64ba0622010-02-24 23:08:47 +0000339 #upload source & doc tarballs
Baptiste Lepilleur1f4847c2010-02-23 07:57:38 +0000340 else:
341 sys.stderr.write( msg + '\n' )
342
343if __name__ == '__main__':
344 main()