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