| # Copyright 2010 Baptiste Lepilleur |
| # Distributed under MIT license, or public domain if desired and |
| # recognized in your jurisdiction. |
| # See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE |
| |
| """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() |