| """Tag the sandbox for release, make source and doc tarballs. | 
 |  | 
 | Requires Python 2.6 | 
 |  | 
 | Example of invocation (use to test the script): | 
 | python makerelease.py --platform=msvc6,msvc71,msvc80,msvc90,mingw -ublep 0.6.0 0.7.0-dev | 
 |  | 
 | When testing this script: | 
 | python makerelease.py --force --retag --platform=msvc6,msvc71,msvc80,mingw -ublep test-0.6.0 test-0.6.1-dev | 
 |  | 
 | Example of invocation when doing a release: | 
 | python makerelease.py 0.5.0 0.6.0-dev | 
 |  | 
 | Note: This was for Subversion. Now that we are in GitHub, we do not | 
 | need to build versioned tarballs anymore, so makerelease.py is defunct. | 
 | """ | 
 | from __future__ import print_function | 
 | import os.path | 
 | import subprocess | 
 | import sys | 
 | import doxybuild | 
 | import subprocess | 
 | import xml.etree.ElementTree as ElementTree | 
 | import shutil | 
 | import urllib2 | 
 | import tempfile | 
 | import os | 
 | import time | 
 | from devtools import antglob, fixeol, tarball | 
 | import amalgamate | 
 |  | 
 | SVN_ROOT = 'https://jsoncpp.svn.sourceforge.net/svnroot/jsoncpp/' | 
 | SVN_TAG_ROOT = SVN_ROOT + 'tags/jsoncpp' | 
 | SCONS_LOCAL_URL = 'http://sourceforge.net/projects/scons/files/scons-local/1.2.0/scons-local-1.2.0.tar.gz/download' | 
 | SOURCEFORGE_PROJECT = 'jsoncpp' | 
 |  | 
 | def set_version( version ): | 
 |     with open('version','wb') as f: | 
 |         f.write( version.strip() ) | 
 |  | 
 | def rmdir_if_exist( dir_path ): | 
 |     if os.path.isdir( dir_path ): | 
 |         shutil.rmtree( dir_path ) | 
 |  | 
 | class SVNError(Exception): | 
 |     pass | 
 |  | 
 | def svn_command( command, *args ): | 
 |     cmd = ['svn', '--non-interactive', command] + list(args) | 
 |     print('Running:', ' '.join( cmd )) | 
 |     process = subprocess.Popen( cmd, | 
 |                                 stdout=subprocess.PIPE, | 
 |                                 stderr=subprocess.STDOUT ) | 
 |     stdout = process.communicate()[0] | 
 |     if process.returncode: | 
 |         error = SVNError( 'SVN command failed:\n' + stdout ) | 
 |         error.returncode = process.returncode | 
 |         raise error | 
 |     return stdout | 
 |  | 
 | def check_no_pending_commit(): | 
 |     """Checks that there is no pending commit in the sandbox.""" | 
 |     stdout = svn_command( 'status', '--xml' ) | 
 |     etree = ElementTree.fromstring( stdout ) | 
 |     msg = [] | 
 |     for entry in etree.getiterator( 'entry' ): | 
 |         path = entry.get('path') | 
 |         status = entry.find('wc-status').get('item') | 
 |         if status != 'unversioned' and path != 'version': | 
 |             msg.append( 'File "%s" has pending change (status="%s")' % (path, status) ) | 
 |     if msg: | 
 |         msg.insert(0, 'Pending change to commit found in sandbox. Commit them first!' ) | 
 |     return '\n'.join( msg ) | 
 |  | 
 | def svn_join_url( base_url, suffix ): | 
 |     if not base_url.endswith('/'): | 
 |         base_url += '/' | 
 |     if suffix.startswith('/'): | 
 |         suffix = suffix[1:] | 
 |     return base_url + suffix | 
 |  | 
 | def svn_check_if_tag_exist( tag_url ): | 
 |     """Checks if a tag exist. | 
 |     Returns: True if the tag exist, False otherwise. | 
 |     """ | 
 |     try: | 
 |         list_stdout = svn_command( 'list', tag_url ) | 
 |     except SVNError as e: | 
 |         if e.returncode != 1 or not str(e).find('tag_url'): | 
 |             raise e | 
 |         # otherwise ignore error, meaning tag does not exist | 
 |         return False | 
 |     return True | 
 |  | 
 | def svn_commit( message ): | 
 |     """Commit the sandbox, providing the specified comment. | 
 |     """ | 
 |     svn_command( 'ci', '-m', message ) | 
 |  | 
 | def svn_tag_sandbox( tag_url, message ): | 
 |     """Makes a tag based on the sandbox revisions. | 
 |     """ | 
 |     svn_command( 'copy', '-m', message, '.', tag_url ) | 
 |  | 
 | def svn_remove_tag( tag_url, message ): | 
 |     """Removes an existing tag. | 
 |     """ | 
 |     svn_command( 'delete', '-m', message, tag_url ) | 
 |  | 
 | def svn_export( tag_url, export_dir ): | 
 |     """Exports the tag_url revision to export_dir. | 
 |        Target directory, including its parent is created if it does not exist. | 
 |        If the directory export_dir exist, it is deleted before export proceed. | 
 |     """ | 
 |     rmdir_if_exist( export_dir ) | 
 |     svn_command( 'export', tag_url, export_dir ) | 
 |  | 
 | def fix_sources_eol( dist_dir ): | 
 |     """Set file EOL for tarball distribution. | 
 |     """ | 
 |     print('Preparing exported source file EOL for distribution...') | 
 |     prune_dirs = antglob.prune_dirs + 'scons-local* ./build* ./libs ./dist' | 
 |     win_sources = antglob.glob( dist_dir,  | 
 |         includes = '**/*.sln **/*.vcproj', | 
 |         prune_dirs = prune_dirs ) | 
 |     unix_sources = antglob.glob( dist_dir, | 
 |         includes = '''**/*.h **/*.cpp **/*.inl **/*.txt **/*.dox **/*.py **/*.html **/*.in | 
 |         sconscript *.json *.expected AUTHORS LICENSE''', | 
 |         excludes = antglob.default_excludes + 'scons.py sconsign.py scons-*', | 
 |         prune_dirs = prune_dirs ) | 
 |     for path in win_sources: | 
 |         fixeol.fix_source_eol( path, is_dry_run = False, verbose = True, eol = '\r\n' ) | 
 |     for path in unix_sources: | 
 |         fixeol.fix_source_eol( path, is_dry_run = False, verbose = True, eol = '\n' ) | 
 |  | 
 | def download( url, target_path ): | 
 |     """Download file represented by url to target_path. | 
 |     """ | 
 |     f = urllib2.urlopen( url ) | 
 |     try: | 
 |         data = f.read() | 
 |     finally: | 
 |         f.close() | 
 |     fout = open( target_path, 'wb' ) | 
 |     try: | 
 |         fout.write( data ) | 
 |     finally: | 
 |         fout.close() | 
 |  | 
 | def check_compile( distcheck_top_dir, platform ): | 
 |     cmd = [sys.executable, 'scons.py', 'platform=%s' % platform, 'check'] | 
 |     print('Running:', ' '.join( cmd )) | 
 |     log_path = os.path.join( distcheck_top_dir, 'build-%s.log' % platform ) | 
 |     flog = open( log_path, 'wb' ) | 
 |     try: | 
 |         process = subprocess.Popen( cmd, | 
 |                                     stdout=flog, | 
 |                                     stderr=subprocess.STDOUT, | 
 |                                     cwd=distcheck_top_dir ) | 
 |         stdout = process.communicate()[0] | 
 |         status = (process.returncode == 0) | 
 |     finally: | 
 |         flog.close() | 
 |     return (status, log_path) | 
 |  | 
 | def write_tempfile( content, **kwargs ): | 
 |     fd, path = tempfile.mkstemp( **kwargs ) | 
 |     f = os.fdopen( fd, 'wt' ) | 
 |     try: | 
 |         f.write( content ) | 
 |     finally: | 
 |         f.close() | 
 |     return path | 
 |  | 
 | class SFTPError(Exception): | 
 |     pass | 
 |  | 
 | def run_sftp_batch( userhost, sftp, batch, retry=0 ): | 
 |     path = write_tempfile( batch, suffix='.sftp', text=True ) | 
 |     # psftp -agent -C blep,jsoncpp@web.sourceforge.net -batch -b batch.sftp -bc | 
 |     cmd = [sftp, '-agent', '-C', '-batch', '-b', path, '-bc', userhost] | 
 |     error = None | 
 |     for retry_index in range(0, max(1,retry)): | 
 |         heading = retry_index == 0 and 'Running:' or 'Retrying:' | 
 |         print(heading, ' '.join( cmd )) | 
 |         process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) | 
 |         stdout = process.communicate()[0] | 
 |         if process.returncode != 0: | 
 |             error = SFTPError( 'SFTP batch failed:\n' + stdout ) | 
 |         else: | 
 |             break | 
 |     if error: | 
 |         raise error | 
 |     return stdout | 
 |  | 
 | def sourceforge_web_synchro( sourceforge_project, doc_dir, | 
 |                              user=None, sftp='sftp' ): | 
 |     """Notes: does not synchronize sub-directory of doc-dir. | 
 |     """ | 
 |     userhost = '%s,%s@web.sourceforge.net' % (user, sourceforge_project) | 
 |     stdout = run_sftp_batch( userhost, sftp, """ | 
 | cd htdocs | 
 | dir | 
 | exit | 
 | """ ) | 
 |     existing_paths = set() | 
 |     collect = 0 | 
 |     for line in stdout.split('\n'): | 
 |         line = line.strip() | 
 |         if not collect and line.endswith('> dir'): | 
 |             collect = True | 
 |         elif collect and line.endswith('> exit'): | 
 |             break | 
 |         elif collect == 1: | 
 |             collect = 2 | 
 |         elif collect == 2: | 
 |             path = line.strip().split()[-1:] | 
 |             if path and path[0] not in ('.', '..'): | 
 |                 existing_paths.add( path[0] ) | 
 |     upload_paths = set( [os.path.basename(p) for p in antglob.glob( doc_dir )] ) | 
 |     paths_to_remove = existing_paths - upload_paths | 
 |     if paths_to_remove: | 
 |         print('Removing the following file from web:') | 
 |         print('\n'.join( paths_to_remove )) | 
 |         stdout = run_sftp_batch( userhost, sftp, """cd htdocs | 
 | rm %s | 
 | exit""" % ' '.join(paths_to_remove) ) | 
 |     print('Uploading %d files:' % len(upload_paths)) | 
 |     batch_size = 10 | 
 |     upload_paths = list(upload_paths) | 
 |     start_time = time.time() | 
 |     for index in range(0,len(upload_paths),batch_size): | 
 |         paths = upload_paths[index:index+batch_size] | 
 |         file_per_sec = (time.time() - start_time) / (index+1) | 
 |         remaining_files = len(upload_paths) - index | 
 |         remaining_sec = file_per_sec * remaining_files | 
 |         print('%d/%d, ETA=%.1fs' % (index+1, len(upload_paths), remaining_sec)) | 
 |         run_sftp_batch( userhost, sftp, """cd htdocs | 
 | lcd %s | 
 | mput %s | 
 | exit""" % (doc_dir, ' '.join(paths) ), retry=3 ) | 
 |  | 
 | def sourceforge_release_tarball( sourceforge_project, paths, user=None, sftp='sftp' ): | 
 |     userhost = '%s,%s@frs.sourceforge.net' % (user, sourceforge_project) | 
 |     run_sftp_batch( userhost, sftp, """ | 
 | mput %s | 
 | exit | 
 | """ % (' '.join(paths),) ) | 
 |  | 
 |  | 
 | def main(): | 
 |     usage = """%prog release_version next_dev_version | 
 | Update 'version' file to release_version and commit. | 
 | Generates the document tarball. | 
 | Tags the sandbox revision with release_version. | 
 | Update 'version' file to next_dev_version and commit. | 
 |  | 
 | Performs an svn export of tag release version, and build a source tarball.     | 
 |  | 
 | Must be started in the project top directory. | 
 |  | 
 | Warning: --force should only be used when developping/testing the release script. | 
 | """ | 
 |     from optparse import OptionParser | 
 |     parser = OptionParser(usage=usage) | 
 |     parser.allow_interspersed_args = False | 
 |     parser.add_option('--dot', dest="dot_path", action='store', default=doxybuild.find_program('dot'), | 
 |         help="""Path to GraphViz dot tool. Must be full qualified path. [Default: %default]""") | 
 |     parser.add_option('--doxygen', dest="doxygen_path", action='store', default=doxybuild.find_program('doxygen'), | 
 |         help="""Path to Doxygen tool. [Default: %default]""") | 
 |     parser.add_option('--force', dest="ignore_pending_commit", action='store_true', default=False, | 
 |         help="""Ignore pending commit. [Default: %default]""") | 
 |     parser.add_option('--retag', dest="retag_release", action='store_true', default=False, | 
 |         help="""Overwrite release existing tag if it exist. [Default: %default]""") | 
 |     parser.add_option('-p', '--platforms', dest="platforms", action='store', default='', | 
 |         help="""Comma separated list of platform passed to scons for build check.""") | 
 |     parser.add_option('--no-test', dest="no_test", action='store_true', default=False, | 
 |         help="""Skips build check.""") | 
 |     parser.add_option('--no-web', dest="no_web", action='store_true', default=False, | 
 |         help="""Do not update web site.""") | 
 |     parser.add_option('-u', '--upload-user', dest="user", action='store', | 
 |                       help="""Sourceforge user for SFTP documentation upload.""") | 
 |     parser.add_option('--sftp', dest='sftp', action='store', default=doxybuild.find_program('psftp', 'sftp'), | 
 |                       help="""Path of the SFTP compatible binary used to upload the documentation.""") | 
 |     parser.enable_interspersed_args() | 
 |     options, args = parser.parse_args() | 
 |  | 
 |     if len(args) != 2: | 
 |         parser.error( 'release_version missing on command-line.' ) | 
 |     release_version = args[0] | 
 |     next_version = args[1] | 
 |  | 
 |     if not options.platforms and not options.no_test: | 
 |         parser.error( 'You must specify either --platform or --no-test option.' ) | 
 |  | 
 |     if options.ignore_pending_commit: | 
 |         msg = '' | 
 |     else: | 
 |         msg = check_no_pending_commit() | 
 |     if not msg: | 
 |         print('Setting version to', release_version) | 
 |         set_version( release_version ) | 
 |         svn_commit( 'Release ' + release_version ) | 
 |         tag_url = svn_join_url( SVN_TAG_ROOT, release_version ) | 
 |         if svn_check_if_tag_exist( tag_url ): | 
 |             if options.retag_release: | 
 |                 svn_remove_tag( tag_url, 'Overwriting previous tag' ) | 
 |             else: | 
 |                 print('Aborting, tag %s already exist. Use --retag to overwrite it!' % tag_url) | 
 |                 sys.exit( 1 ) | 
 |         svn_tag_sandbox( tag_url, 'Release ' + release_version ) | 
 |  | 
 |         print('Generated doxygen document...') | 
 | ##        doc_dirname = r'jsoncpp-api-html-0.5.0' | 
 | ##        doc_tarball_path = r'e:\prg\vc\Lib\jsoncpp-trunk\dist\jsoncpp-api-html-0.5.0.tar.gz' | 
 |         doc_tarball_path, doc_dirname = doxybuild.build_doc( options, make_release=True ) | 
 |         doc_distcheck_dir = 'dist/doccheck' | 
 |         tarball.decompress( doc_tarball_path, doc_distcheck_dir ) | 
 |         doc_distcheck_top_dir = os.path.join( doc_distcheck_dir, doc_dirname ) | 
 |          | 
 |         export_dir = 'dist/export' | 
 |         svn_export( tag_url, export_dir ) | 
 |         fix_sources_eol( export_dir ) | 
 |          | 
 |         source_dir = 'jsoncpp-src-' + release_version | 
 |         source_tarball_path = 'dist/%s.tar.gz' % source_dir | 
 |         print('Generating source tarball to', source_tarball_path) | 
 |         tarball.make_tarball( source_tarball_path, [export_dir], export_dir, prefix_dir=source_dir ) | 
 |  | 
 |         amalgamation_tarball_path = 'dist/%s-amalgamation.tar.gz' % source_dir | 
 |         print('Generating amalgamation source tarball to', amalgamation_tarball_path) | 
 |         amalgamation_dir = 'dist/amalgamation' | 
 |         amalgamate.amalgamate_source( export_dir, '%s/jsoncpp.cpp' % amalgamation_dir, 'json/json.h' ) | 
 |         amalgamation_source_dir = 'jsoncpp-src-amalgamation' + release_version | 
 |         tarball.make_tarball( amalgamation_tarball_path, [amalgamation_dir], | 
 |                               amalgamation_dir, prefix_dir=amalgamation_source_dir ) | 
 |  | 
 |         # Decompress source tarball, download and install scons-local | 
 |         distcheck_dir = 'dist/distcheck' | 
 |         distcheck_top_dir = distcheck_dir + '/' + source_dir | 
 |         print('Decompressing source tarball to', distcheck_dir) | 
 |         rmdir_if_exist( distcheck_dir ) | 
 |         tarball.decompress( source_tarball_path, distcheck_dir ) | 
 |         scons_local_path = 'dist/scons-local.tar.gz' | 
 |         print('Downloading scons-local to', scons_local_path) | 
 |         download( SCONS_LOCAL_URL, scons_local_path ) | 
 |         print('Decompressing scons-local to', distcheck_top_dir) | 
 |         tarball.decompress( scons_local_path, distcheck_top_dir ) | 
 |  | 
 |         # Run compilation | 
 |         print('Compiling decompressed tarball') | 
 |         all_build_status = True | 
 |         for platform in options.platforms.split(','): | 
 |             print('Testing platform:', platform) | 
 |             build_status, log_path = check_compile( distcheck_top_dir, platform ) | 
 |             print('see build log:', log_path) | 
 |             print(build_status and '=> ok' or '=> FAILED') | 
 |             all_build_status = all_build_status and build_status | 
 |         if not build_status: | 
 |             print('Testing failed on at least one platform, aborting...') | 
 |             svn_remove_tag( tag_url, 'Removing tag due to failed testing' ) | 
 |             sys.exit(1) | 
 |         if options.user: | 
 |             if not options.no_web: | 
 |                 print('Uploading documentation using user', options.user) | 
 |                 sourceforge_web_synchro( SOURCEFORGE_PROJECT, doc_distcheck_top_dir, user=options.user, sftp=options.sftp ) | 
 |                 print('Completed documentation upload') | 
 |             print('Uploading source and documentation tarballs for release using user', options.user) | 
 |             sourceforge_release_tarball( SOURCEFORGE_PROJECT, | 
 |                                          [source_tarball_path, doc_tarball_path], | 
 |                                          user=options.user, sftp=options.sftp ) | 
 |             print('Source and doc release tarballs uploaded') | 
 |         else: | 
 |             print('No upload user specified. Web site and download tarbal were not uploaded.') | 
 |             print('Tarball can be found at:', doc_tarball_path) | 
 |  | 
 |         # Set next version number and commit             | 
 |         set_version( next_version ) | 
 |         svn_commit( 'Released ' + release_version ) | 
 |     else: | 
 |         sys.stderr.write( msg + '\n' ) | 
 |   | 
 | if __name__ == '__main__': | 
 |     main() |