1#!/usr/bin/env python 2 3"""Manage site and releases. 4 5Usage: 6 manage.py release [<branch>] 7 manage.py site 8""" 9 10from __future__ import print_function 11import datetime, docopt, fileinput, json, os 12import re, requests, shutil, sys, tempfile 13from contextlib import contextmanager 14from distutils.version import LooseVersion 15from subprocess import check_call 16 17 18class Git: 19 def __init__(self, dir): 20 self.dir = dir 21 22 def call(self, method, args, **kwargs): 23 return check_call(['git', method] + list(args), **kwargs) 24 25 def add(self, *args): 26 return self.call('add', args, cwd=self.dir) 27 28 def checkout(self, *args): 29 return self.call('checkout', args, cwd=self.dir) 30 31 def clean(self, *args): 32 return self.call('clean', args, cwd=self.dir) 33 34 def clone(self, *args): 35 return self.call('clone', list(args) + [self.dir]) 36 37 def commit(self, *args): 38 return self.call('commit', args, cwd=self.dir) 39 40 def pull(self, *args): 41 return self.call('pull', args, cwd=self.dir) 42 43 def push(self, *args): 44 return self.call('push', args, cwd=self.dir) 45 46 def reset(self, *args): 47 return self.call('reset', args, cwd=self.dir) 48 49 def update(self, *args): 50 clone = not os.path.exists(self.dir) 51 if clone: 52 self.clone(*args) 53 return clone 54 55 56def clean_checkout(repo, branch): 57 repo.clean('-f', '-d') 58 repo.reset('--hard') 59 repo.checkout(branch) 60 61 62class Runner: 63 def __init__(self, cwd): 64 self.cwd = cwd 65 66 def __call__(self, *args, **kwargs): 67 kwargs['cwd'] = kwargs.get('cwd', self.cwd) 68 check_call(args, **kwargs) 69 70 71def create_build_env(): 72 """Create a build environment.""" 73 class Env: 74 pass 75 env = Env() 76 77 # Import the documentation build module. 78 env.fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 79 sys.path.insert(0, os.path.join(env.fmt_dir, 'doc')) 80 import build 81 82 env.build_dir = 'build' 83 84 # Virtualenv and repos are cached to speed up builds. 85 build.create_build_env(os.path.join(env.build_dir, 'virtualenv')) 86 87 env.fmt_repo = Git(os.path.join(env.build_dir, 'fmt')) 88 return env 89 90 91@contextmanager 92def rewrite(filename): 93 class Buffer: 94 pass 95 buffer = Buffer() 96 if not os.path.exists(filename): 97 buffer.data = '' 98 yield buffer 99 return 100 with open(filename) as f: 101 buffer.data = f.read() 102 yield buffer 103 with open(filename, 'w') as f: 104 f.write(buffer.data) 105 106 107fmt_repo_url = 'git@github.com:fmtlib/fmt' 108 109 110def update_site(env): 111 env.fmt_repo.update(fmt_repo_url) 112 113 doc_repo = Git(os.path.join(env.build_dir, 'fmtlib.github.io')) 114 doc_repo.update('git@github.com:fmtlib/fmtlib.github.io') 115 116 for version in ['1.0.0', '1.1.0', '2.0.0', '3.0.0']: 117 clean_checkout(env.fmt_repo, version) 118 target_doc_dir = os.path.join(env.fmt_repo.dir, 'doc') 119 # Remove the old theme. 120 for entry in os.listdir(target_doc_dir): 121 path = os.path.join(target_doc_dir, entry) 122 if os.path.isdir(path): 123 shutil.rmtree(path) 124 # Copy the new theme. 125 for entry in ['_static', '_templates', 'basic-bootstrap', 'bootstrap', 126 'conf.py', 'fmt.less']: 127 src = os.path.join(env.fmt_dir, 'doc', entry) 128 dst = os.path.join(target_doc_dir, entry) 129 copy = shutil.copytree if os.path.isdir(src) else shutil.copyfile 130 copy(src, dst) 131 # Rename index to contents. 132 contents = os.path.join(target_doc_dir, 'contents.rst') 133 if not os.path.exists(contents): 134 os.rename(os.path.join(target_doc_dir, 'index.rst'), contents) 135 # Fix issues in reference.rst/api.rst. 136 for filename in ['reference.rst', 'api.rst']: 137 pattern = re.compile('doxygenfunction.. (bin|oct|hexu|hex)$', re.M) 138 with rewrite(os.path.join(target_doc_dir, filename)) as b: 139 b.data = b.data.replace('std::ostream &', 'std::ostream&') 140 b.data = re.sub(pattern, r'doxygenfunction:: \1(int)', b.data) 141 b.data = b.data.replace('std::FILE*', 'std::FILE *') 142 b.data = b.data.replace('unsigned int', 'unsigned') 143 # Fix a broken link in index.rst. 144 index = os.path.join(target_doc_dir, 'index.rst') 145 with rewrite(index) as b: 146 b.data = b.data.replace( 147 'doc/latest/index.html#format-string-syntax', 'syntax.html') 148 # Build the docs. 149 html_dir = os.path.join(env.build_dir, 'html') 150 if os.path.exists(html_dir): 151 shutil.rmtree(html_dir) 152 include_dir = env.fmt_repo.dir 153 if LooseVersion(version) >= LooseVersion('3.0.0'): 154 include_dir = os.path.join(include_dir, 'fmt') 155 import build 156 build.build_docs(version, doc_dir=target_doc_dir, 157 include_dir=include_dir, work_dir=env.build_dir) 158 shutil.rmtree(os.path.join(html_dir, '.doctrees')) 159 # Create symlinks for older versions. 160 for link, target in {'index': 'contents', 'api': 'reference'}.items(): 161 link = os.path.join(html_dir, link) + '.html' 162 target += '.html' 163 if os.path.exists(os.path.join(html_dir, target)) and \ 164 not os.path.exists(link): 165 os.symlink(target, link) 166 # Copy docs to the website. 167 version_doc_dir = os.path.join(doc_repo.dir, version) 168 shutil.rmtree(version_doc_dir) 169 shutil.move(html_dir, version_doc_dir) 170 171 172def release(args): 173 env = create_build_env() 174 fmt_repo = env.fmt_repo 175 176 branch = args.get('<branch>') 177 if branch is None: 178 branch = 'master' 179 if not fmt_repo.update('-b', branch, fmt_repo_url): 180 clean_checkout(fmt_repo, branch) 181 182 # Convert changelog from RST to GitHub-flavored Markdown and get the 183 # version. 184 changelog = 'ChangeLog.rst' 185 changelog_path = os.path.join(fmt_repo.dir, changelog) 186 import rst2md 187 changes, version = rst2md.convert(changelog_path) 188 cmakelists = 'CMakeLists.txt' 189 for line in fileinput.input(os.path.join(fmt_repo.dir, cmakelists), 190 inplace=True): 191 prefix = 'set(FMT_VERSION ' 192 if line.startswith(prefix): 193 line = prefix + version + ')\n' 194 sys.stdout.write(line) 195 196 # Update the version in the changelog. 197 title_len = 0 198 for line in fileinput.input(changelog_path, inplace=True): 199 if line.decode('utf-8').startswith(version + ' - TBD'): 200 line = version + ' - ' + datetime.date.today().isoformat() 201 title_len = len(line) 202 line += '\n' 203 elif title_len: 204 line = '-' * title_len + '\n' 205 title_len = 0 206 sys.stdout.write(line) 207 # TODO: add new version to manage.py 208 fmt_repo.checkout('-B', 'release') 209 fmt_repo.add(changelog, cmakelists) 210 fmt_repo.commit('-m', 'Update version') 211 212 # Build the docs and package. 213 run = Runner(fmt_repo.dir) 214 run('cmake', '.') 215 run('make', 'doc', 'package_source') 216 217 update_site(env) 218 219 # Create a release on GitHub. 220 fmt_repo.push('origin', 'release') 221 r = requests.post('https://api.github.com/repos/fmtlib/fmt/releases', 222 params={'access_token': os.getenv('FMT_TOKEN')}, 223 data=json.dumps({'tag_name': version, 224 'target_commitish': 'release', 225 'body': changes, 'draft': True})) 226 if r.status_code != 201: 227 raise Exception('Failed to create a release ' + str(r)) 228 229 230if __name__ == '__main__': 231 args = docopt.docopt(__doc__) 232 if args.get('release'): 233 release(args) 234 elif args.get('site'): 235 update_site(create_build_env()) 236