diff --git a/support/manage.py b/support/manage.py new file mode 100755 index 00000000..d35598ba --- /dev/null +++ b/support/manage.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python + +"""Manage site and releases. + +Usage: + manage.py release [] + manage.py site +""" + +from __future__ import print_function +import datetime, docopt, fileinput, json, os +import re, requests, shutil, sys, tempfile +from distutils.version import LooseVersion +from subprocess import check_call + + +class Git: + def __init__(self, dir): + self.dir = dir + + def call(self, method, args, **kwargs): + return check_call(['git', method] + list(args), **kwargs) + + def clone(self, *args): + return self.call('clone', list(args) + [self.dir]) + + def checkout(self, *args): + return self.call('checkout', args, cwd=self.dir) + + def clean(self, *args): + return self.call('clean', args, cwd=self.dir) + + def reset(self, *args): + return self.call('reset', args, cwd=self.dir) + + def pull(self, *args): + return self.call('pull', args, cwd=self.dir) + + def update(self, *args): + if not os.path.exists(self.dir): + self.clone(*args) + + +class Runner: + def __init__(self): + self.cwd = '.' + + def __call__(self, *args, **kwargs): + kwargs['cwd'] = kwargs.get('cwd', self.cwd) + check_call(args, **kwargs) + + +def create_build_env(): + """Create a build environment.""" + class Env: + pass + env = Env() + + # Import the documentation build module. + env.fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, os.path.join(env.fmt_dir, 'doc')) + import build + + env.build_dir = 'build' + + # Virtualenv and repos are cached to speed up builds. + build.create_build_env(os.path.join(env.build_dir, 'virtualenv')) + + env.fmt_repo = Git(os.path.join(env.build_dir, 'fmt')) + return env + + +fmt_repo_url = 'git@github.com:fmtlib/fmt' + + +def update_site(env): + env.fmt_repo.update(fmt_repo_url) + + doc_repo = Git(os.path.join(env.build_dir, 'fmtlib.github.io')) + doc_repo.update('git@github.com:fmtlib/fmtlib.github.io') + + for version in ['1.0.0', '1.1.0', '2.0.0', '3.0.0']: + env.fmt_repo.clean('-f', '-d') + env.fmt_repo.reset('--hard') + env.fmt_repo.checkout(version) + target_doc_dir = os.path.join(env.fmt_repo.dir, 'doc') + # Remove the old theme. + for entry in os.listdir(target_doc_dir): + path = os.path.join(target_doc_dir, entry) + if os.path.isdir(path): + shutil.rmtree(path) + # Copy the new theme. + for entry in ['_static', '_templates', 'basic-bootstrap', 'bootstrap', + 'conf.py', 'fmt.less']: + src = os.path.join(env.fmt_dir, 'doc', entry) + dst = os.path.join(target_doc_dir, entry) + copy = shutil.copytree if os.path.isdir(src) else shutil.copyfile + copy(src, dst) + # Rename index to contents. + contents = os.path.join(target_doc_dir, 'contents.rst') + if not os.path.exists(contents): + os.rename(os.path.join(target_doc_dir, 'index.rst'), contents) + # Fix issues in reference.rst/api.rst. + for filename in ['reference.rst', 'api.rst']: + reference = os.path.join(target_doc_dir, filename) + if not os.path.exists(reference): + continue + with open(reference) as f: + data = f.read() + data = data.replace('std::ostream &', 'std::ostream&') + pattern = re.compile('doxygenfunction.. (bin|oct|hexu|hex)$', re.M) + data = re.sub(pattern, r'doxygenfunction:: \1(int)', data) + data = data.replace('std::FILE*', 'std::FILE *') + data = data.replace('unsigned int', 'unsigned') + with open(reference, 'w') as f: + f.write(data) + # Build the docs. + html_dir = os.path.join(env.build_dir, 'html') + if os.path.exists(html_dir): + shutil.rmtree(html_dir) + include_dir = env.fmt_repo.dir + if LooseVersion(version) >= LooseVersion('3.0.0'): + include_dir = os.path.join(include_dir, 'fmt') + import build + build.build_docs(version, doc_dir=target_doc_dir, + include_dir=include_dir, work_dir=env.build_dir) + shutil.rmtree(os.path.join(html_dir, '.doctrees')) + # Create symlinks for older versions. + for link, target in {'index': 'contents', 'api': 'reference'}.items(): + link = os.path.join(html_dir, link) + '.html' + target += '.html' + if os.path.exists(os.path.join(html_dir, target)) and \ + not os.path.exists(link): + os.symlink(target, link) + # Copy docs to the website. + version_doc_dir = os.path.join(doc_repo.dir, version) + shutil.rmtree(version_doc_dir) + shutil.move(html_dir, version_doc_dir) + + +def release(args): + env = create_build_env() + + branch = args.get('') + if branch is None: + branch = 'master' + env.fmt_repo.update('-b', branch, fmt_repo_url) + + # Convert changelog from RST to GitHub-flavored Markdown and get the + # version. + changelog = 'ChangeLog.rst' + changelog_path = os.path.join(env.fmt_repo.dir, changelog) + import rst2md + changes, version = rst2md.convert(changelog_path) + cmakelists = 'CMakeLists.txt' + for line in fileinput.input(os.path.join(env.fmt_repo.dir, cmakelists), + inplace=True): + prefix = 'set(FMT_VERSION ' + if line.startswith(prefix): + line = prefix + version + ')\n' + sys.stdout.write(line) + + # Update the version in the changelog. + title_len = 0 + for line in fileinput.input(changelog_path, inplace=True): + if line.decode('utf-8').startswith(version + ' - TBD'): + line = version + ' - ' + datetime.date.today().isoformat() + title_len = len(line) + line += '\n' + elif title_len: + line = '-' * title_len + '\n' + title_len = 0 + sys.stdout.write(line) + run = Runner() + run.cwd = env.fmt_repo.dir + run('git', 'checkout', '-b', 'release') + run('git', 'add', changelog, cmakelists) + run('git', 'commit', '-m', 'Update version') + + # Build the docs and package. + run('cmake', '.') + run('make', 'doc', 'package_source') + + update_site(env) + + # Create a release on GitHub. + run('git', 'push', 'origin', 'release', cwd=env.fmt_repo.dir) + r = requests.post('https://api.github.com/repos/fmtlib/fmt/releases', + params={'access_token': os.getenv('FMT_TOKEN')}, + data=json.dumps({'tag_name': version, + 'target_commitish': 'release', + 'body': changes, 'draft': True})) + if r.status_code != 201: + raise Exception('Failed to create a release ' + str(r)) + + +if __name__ == '__main__': + args = docopt.docopt(__doc__) + if args.get('release'): + release(args) + elif args.get('site'): + update_site(create_build_env()) diff --git a/support/release.py b/support/release.py deleted file mode 100755 index 3526e484..00000000 --- a/support/release.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python - -"""Create a release. - -Usage: - release.py [] -""" - -from __future__ import print_function -import datetime, docopt, fileinput, json, os, re, requests, shutil, sys, tempfile -from docutils import nodes, writers, core -from subprocess import check_call - -class MDWriter(writers.Writer): - """GitHub-flavored markdown writer""" - - supported = ('md',) - """Formats this writer supports.""" - - def translate(self): - translator = Translator(self.document) - self.document.walkabout(translator) - self.output = (translator.output, translator.version) - - -def is_github_ref(node): - return re.match('https://github.com/.*/(issues|pull)/.*', node['refuri']) - - -class Translator(nodes.NodeVisitor): - def __init__(self, document): - nodes.NodeVisitor.__init__(self, document) - self.output = '' - self.indent = 0 - self.preserve_newlines = False - - def write(self, text): - self.output += text.replace('\n', '\n' + ' ' * self.indent) - - def visit_document(self, node): - pass - - def depart_document(self, node): - pass - - def visit_section(self, node): - pass - - def depart_section(self, node): - # Skip all sections except the first one. - raise nodes.StopTraversal - - def visit_title(self, node): - self.version = re.match(r'(\d+\.\d+\.\d+).*', node.children[0]).group(1) - raise nodes.SkipChildren - - def depart_title(self, node): - pass - - def visit_Text(self, node): - if not self.preserve_newlines: - node = node.replace('\n', ' ') - self.write(node) - - def depart_Text(self, node): - pass - - def visit_bullet_list(self, node): - pass - - def depart_bullet_list(self, node): - pass - - def visit_list_item(self, node): - self.write('* ') - self.indent += 2 - - def depart_list_item(self, node): - self.indent -= 2 - self.write('\n\n') - - def visit_paragraph(self, node): - pass - - def depart_paragraph(self, node): - pass - - def visit_reference(self, node): - if not is_github_ref(node): - self.write('[') - - def depart_reference(self, node): - if not is_github_ref(node): - self.write('](' + node['refuri'] + ')') - - def visit_target(self, node): - pass - - def depart_target(self, node): - pass - - def visit_literal(self, node): - self.write('`') - - def depart_literal(self, node): - self.write('`') - - def visit_literal_block(self, node): - self.write('\n\n```') - if 'c++' in node['classes']: - self.write('c++') - self.write('\n') - self.preserve_newlines = True - - def depart_literal_block(self, node): - self.write('\n```\n') - self.preserve_newlines = False - - def visit_inline(self, node): - pass - - def depart_inline(self, node): - pass - - -class Runner: - def __init__(self): - self.cwd = '.' - - def __call__(self, *args, **kwargs): - kwargs['cwd'] = kwargs.get('cwd', self.cwd) - check_call(args, **kwargs) - -if __name__ == '__main__': - args = docopt.docopt(__doc__) - workdir = tempfile.mkdtemp() - try: - run = Runner() - fmt_dir = os.path.join(workdir, 'fmt') - branch = args.get('') - if branch is None: - branch = 'master' - run('git', 'clone', '-b', branch, 'git@github.com:fmtlib/fmt.git', fmt_dir) - - # Convert changelog from RST to GitHub-flavored Markdown and get the version. - changelog = 'ChangeLog.rst' - changelog_path = os.path.join(fmt_dir, changelog) - changes, version = core.publish_file(source_path=changelog_path, writer=MDWriter()) - cmakelists = 'CMakeLists.txt' - for line in fileinput.input(os.path.join(fmt_dir, cmakelists), inplace=True): - prefix = 'set(FMT_VERSION ' - if line.startswith(prefix): - line = prefix + version + ')\n' - sys.stdout.write(line) - - # Update the version in the changelog. - title_len = 0 - for line in fileinput.input(changelog_path, inplace=True): - if line.decode('utf-8').startswith(version + ' - TBD'): - line = version + ' - ' + datetime.date.today().isoformat() - title_len = len(line) - line += '\n' - elif title_len: - line = '-' * title_len + '\n' - title_len = 0 - sys.stdout.write(line) - run.cwd = fmt_dir - run('git', 'checkout', '-b', 'release') - run('git', 'add', changelog, cmakelists) - run('git', 'commit', '-m', 'Update version') - - # Build the docs and package. - run('cmake', '.') - run('make', 'doc', 'package_source') - site_dir = os.path.join(workdir, 'fmtlib.github.io') - run('git', 'clone', 'git@github.com:fmtlib/fmtlib.github.io.git', site_dir) - doc_dir = os.path.join(site_dir, version) - shutil.copytree(os.path.join(fmt_dir, 'doc', 'html'), doc_dir, - ignore=shutil.ignore_patterns('.doctrees', '.buildinfo')) - run.cwd = site_dir - run('git', 'add', doc_dir) - run('git', 'commit', '-m', 'Update docs') - - # Create a release on GitHub. - run('git', 'push', 'origin', 'release', cwd=fmt_dir) - r = requests.post('https://api.github.com/repos/fmtlib/fmt/releases', - params={'access_token': os.getenv('FMT_TOKEN')}, - data=json.dumps({'tag_name': version, 'target_commitish': 'release', - 'body': changes, 'draft': True})) - if r.status_code != 201: - raise Exception('Failed to create a release ' + str(r)) - finally: - shutil.rmtree(workdir) diff --git a/support/rst2md.py b/support/rst2md.py new file mode 100644 index 00000000..6d1da6e0 --- /dev/null +++ b/support/rst2md.py @@ -0,0 +1,121 @@ +# reStructuredText (RST) to GitHub-flavored Markdown converter + +import re +from docutils import core, nodes, writers + + +def is_github_ref(node): + return re.match('https://github.com/.*/(issues|pull)/.*', node['refuri']) + + +class Translator(nodes.NodeVisitor): + def __init__(self, document): + nodes.NodeVisitor.__init__(self, document) + self.output = '' + self.indent = 0 + self.preserve_newlines = False + + def write(self, text): + self.output += text.replace('\n', '\n' + ' ' * self.indent) + + def visit_document(self, node): + pass + + def depart_document(self, node): + pass + + def visit_section(self, node): + pass + + def depart_section(self, node): + # Skip all sections except the first one. + raise nodes.StopTraversal + + def visit_title(self, node): + self.version = re.match(r'(\d+\.\d+\.\d+).*', node.children[0]).group(1) + raise nodes.SkipChildren + + def depart_title(self, node): + pass + + def visit_Text(self, node): + if not self.preserve_newlines: + node = node.replace('\n', ' ') + self.write(node) + + def depart_Text(self, node): + pass + + def visit_bullet_list(self, node): + pass + + def depart_bullet_list(self, node): + pass + + def visit_list_item(self, node): + self.write('* ') + self.indent += 2 + + def depart_list_item(self, node): + self.indent -= 2 + self.write('\n\n') + + def visit_paragraph(self, node): + pass + + def depart_paragraph(self, node): + pass + + def visit_reference(self, node): + if not is_github_ref(node): + self.write('[') + + def depart_reference(self, node): + if not is_github_ref(node): + self.write('](' + node['refuri'] + ')') + + def visit_target(self, node): + pass + + def depart_target(self, node): + pass + + def visit_literal(self, node): + self.write('`') + + def depart_literal(self, node): + self.write('`') + + def visit_literal_block(self, node): + self.write('\n\n```') + if 'c++' in node['classes']: + self.write('c++') + self.write('\n') + self.preserve_newlines = True + + def depart_literal_block(self, node): + self.write('\n```\n') + self.preserve_newlines = False + + def visit_inline(self, node): + pass + + def depart_inline(self, node): + pass + + +class MDWriter(writers.Writer): + """GitHub-flavored markdown writer""" + + supported = ('md',) + """Formats this writer supports.""" + + def translate(self): + translator = Translator(self.document) + self.document.walkabout(translator) + self.output = (translator.output, translator.version) + + +def convert(rst_path): + """Converts RST file to Markdown.""" + return core.publish_file(source_path=rst_path, writer=MDWriter()) diff --git a/support/update-website.py b/support/update-website.py deleted file mode 100755 index c5b66e9f..00000000 --- a/support/update-website.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python - -import os, re, shutil, sys -from distutils.version import LooseVersion -from subprocess import check_call - -class Git: - def __init__(self, dir): - self.dir = dir - - def call(self, method, args, **kwargs): - return check_call(['git', method] + list(args), **kwargs) - - def clone(self, *args): - return self.call('clone', list(args) + [self.dir]) - - def checkout(self, *args): - return self.call('checkout', args, cwd=self.dir) - - def clean(self, *args): - return self.call('clean', args, cwd=self.dir) - - def reset(self, *args): - return self.call('reset', args, cwd=self.dir) - - def pull(self, *args): - return self.call('pull', args, cwd=self.dir) - - def update(self, *args): - if not os.path.exists(self.dir): - self.clone(*args) - -# Import the documentation build module. -fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, os.path.join(fmt_dir, 'doc')) -import build - -build_dir = 'build' - -# Virtualenv and repos are cached to speed up builds. -build.create_build_env(os.path.join(build_dir, 'virtualenv')) - -fmt_repo = Git(os.path.join(build_dir, 'fmt')) -fmt_repo.update('git@github.com:fmtlib/fmt') - -doc_repo = Git(os.path.join(build_dir, 'fmtlib.github.io')) -doc_repo.update('git@github.com:fmtlib/fmtlib.github.io') - -for version in ['1.0.0', '1.1.0', '2.0.0', '3.0.0']: - fmt_repo.clean('-f', '-d') - fmt_repo.reset('--hard') - fmt_repo.checkout(version) - target_doc_dir = os.path.join(fmt_repo.dir, 'doc') - # Remove the old theme. - for entry in os.listdir(target_doc_dir): - path = os.path.join(target_doc_dir, entry) - if os.path.isdir(path): - shutil.rmtree(path) - # Copy the new theme. - for entry in ['_static', '_templates', 'basic-bootstrap', 'bootstrap', - 'conf.py', 'fmt.less']: - src = os.path.join(fmt_dir, 'doc', entry) - dst = os.path.join(target_doc_dir, entry) - copy = shutil.copytree if os.path.isdir(src) else shutil.copyfile - copy(src, dst) - # Rename index to contents. - contents = os.path.join(target_doc_dir, 'contents.rst') - if not os.path.exists(contents): - os.rename(os.path.join(target_doc_dir, 'index.rst'), contents) - # Fix issues in reference.rst/api.rst. - for filename in ['reference.rst', 'api.rst']: - reference = os.path.join(target_doc_dir, filename) - if not os.path.exists(reference): - continue - with open(reference) as f: - data = f.read() - data = data.replace('std::ostream &', 'std::ostream&') - pat = re.compile('doxygenfunction.. (bin|oct|hexu|hex)$', re.MULTILINE) - data = re.sub(pat, r'doxygenfunction:: \1(int)', data) - data = data.replace('std::FILE*', 'std::FILE *') - data = data.replace('unsigned int', 'unsigned') - with open(reference, 'w') as f: - f.write(data) - # Build the docs. - html_dir = os.path.join(build_dir, 'html') - if os.path.exists(html_dir): - shutil.rmtree(html_dir) - include_dir = fmt_repo.dir - if LooseVersion(version) >= LooseVersion('3.0.0'): - include_dir = os.path.join(include_dir, 'fmt') - build.build_docs(version, doc_dir=target_doc_dir, - include_dir=include_dir, work_dir=build_dir) - shutil.rmtree(os.path.join(html_dir, '.doctrees')) - # Create symlinks for older versions. - for link, target in {'index': 'contents', 'api': 'reference'}.items(): - link = os.path.join(html_dir, link) + '.html' - target += '.html' - if os.path.exists(os.path.join(html_dir, target)) and \ - not os.path.exists(link): - os.symlink(target, link) - # Copy docs to the website. - version_doc_dir = os.path.join(doc_repo.dir, version) - shutil.rmtree(version_doc_dir) - shutil.move(html_dir, version_doc_dir)