1#!/usr/bin/env python3 2 3"""Manage site and releases. 4 5Usage: 6 manage.py release [<branch>] 7 manage.py site 8 9For the release command $FMT_TOKEN should contain a GitHub personal access token 10obtained from https://github.com/settings/tokens. 11""" 12 13from __future__ import print_function 14import datetime, docopt, errno, fileinput, json, os 15import re, requests, shutil, sys 16from contextlib import contextmanager 17from subprocess import check_call 18 19 20class Git: 21 def __init__(self, dir): 22 self.dir = dir 23 24 def call(self, method, args, **kwargs): 25 return check_call(['git', method] + list(args), **kwargs) 26 27 def add(self, *args): 28 return self.call('add', args, cwd=self.dir) 29 30 def checkout(self, *args): 31 return self.call('checkout', args, cwd=self.dir) 32 33 def clean(self, *args): 34 return self.call('clean', args, cwd=self.dir) 35 36 def clone(self, *args): 37 return self.call('clone', list(args) + [self.dir]) 38 39 def commit(self, *args): 40 return self.call('commit', args, cwd=self.dir) 41 42 def pull(self, *args): 43 return self.call('pull', args, cwd=self.dir) 44 45 def push(self, *args): 46 return self.call('push', args, cwd=self.dir) 47 48 def reset(self, *args): 49 return self.call('reset', args, cwd=self.dir) 50 51 def update(self, *args): 52 clone = not os.path.exists(self.dir) 53 if clone: 54 self.clone(*args) 55 return clone 56 57 58def clean_checkout(repo, branch): 59 repo.clean('-f', '-d') 60 repo.reset('--hard') 61 repo.checkout(branch) 62 63 64class Runner: 65 def __init__(self, cwd): 66 self.cwd = cwd 67 68 def __call__(self, *args, **kwargs): 69 kwargs['cwd'] = kwargs.get('cwd', self.cwd) 70 check_call(args, **kwargs) 71 72 73def create_build_env(): 74 """Create a build environment.""" 75 class Env: 76 pass 77 env = Env() 78 env.fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 79 env.build_dir = 'build' 80 env.fmt_repo = Git(os.path.join(env.build_dir, 'fmt')) 81 return env 82 83 84fmt_repo_url = '[email protected]:fmtlib/fmt' 85 86 87def update_site(env): 88 env.fmt_repo.update(fmt_repo_url) 89 90 doc_repo = Git(os.path.join(env.build_dir, 'fmt.dev')) 91 doc_repo.update('[email protected]:fmtlib/fmt.dev') 92 93 version = '11.0.0' 94 clean_checkout(env.fmt_repo, version) 95 target_doc_dir = os.path.join(env.fmt_repo.dir, 'doc') 96 97 # Build the docs. 98 html_dir = os.path.join(env.build_dir, 'html') 99 if os.path.exists(html_dir): 100 shutil.rmtree(html_dir) 101 include_dir = env.fmt_repo.dir 102 import build 103 build.build_docs(version, doc_dir=target_doc_dir, 104 include_dir=include_dir, work_dir=env.build_dir) 105 shutil.rmtree(os.path.join(html_dir, '.doctrees')) 106 # Copy docs to the website. 107 version_doc_dir = os.path.join(doc_repo.dir, version) 108 try: 109 shutil.rmtree(version_doc_dir) 110 except OSError as e: 111 if e.errno != errno.ENOENT: 112 raise 113 shutil.move(html_dir, version_doc_dir) 114 115 116def release(args): 117 env = create_build_env() 118 fmt_repo = env.fmt_repo 119 120 branch = args.get('<branch>') 121 if branch is None: 122 branch = 'master' 123 if not fmt_repo.update('-b', branch, fmt_repo_url): 124 clean_checkout(fmt_repo, branch) 125 126 # Update the date in the changelog and extract the version and the first 127 # section content. 128 changelog = 'ChangeLog.md' 129 changelog_path = os.path.join(fmt_repo.dir, changelog) 130 is_first_section = True 131 first_section = [] 132 for i, line in enumerate(fileinput.input(changelog_path, inplace=True)): 133 if i == 0: 134 version = re.match(r'# (.*) - TBD', line).group(1) 135 line = '# {} - {}\n'.format( 136 version, datetime.date.today().isoformat()) 137 elif not is_first_section: 138 pass 139 elif line.startswith('#'): 140 is_first_section = False 141 else: 142 first_section.append(line) 143 sys.stdout.write(line) 144 if first_section[0] == '\n': 145 first_section.pop(0) 146 147 ns_version = None 148 base_h_path = os.path.join(fmt_repo.dir, 'include', 'fmt', 'base.h') 149 for line in fileinput.input(base_h_path): 150 m = re.match(r'\s*inline namespace v(.*) .*', line) 151 if m: 152 ns_version = m.group(1) 153 break 154 major_version = version.split('.')[0] 155 if not ns_version or ns_version != major_version: 156 raise Exception(f'Version mismatch {ns_version} != {major_version}') 157 158 # Workaround GitHub-flavored Markdown treating newlines as <br>. 159 changes = '' 160 code_block = False 161 stripped = False 162 for line in first_section: 163 if re.match(r'^\s*```', line): 164 code_block = not code_block 165 changes += line 166 stripped = False 167 continue 168 if code_block: 169 changes += line 170 continue 171 if line == '\n' or re.match(r'^\s*\|.*', line): 172 if stripped: 173 changes += '\n' 174 stripped = False 175 changes += line 176 continue 177 if stripped: 178 line = ' ' + line.lstrip() 179 changes += line.rstrip() 180 stripped = True 181 182 fmt_repo.checkout('-B', 'release') 183 fmt_repo.add(changelog) 184 fmt_repo.commit('-m', 'Update version') 185 186 # Build the docs and package. 187 run = Runner(fmt_repo.dir) 188 run('cmake', '.') 189 run('make', 'doc', 'package_source') 190 191 # Create a release on GitHub. 192 fmt_repo.push('origin', 'release') 193 auth_headers = {'Authorization': 'token ' + os.getenv('FMT_TOKEN')} 194 r = requests.post('https://api.github.com/repos/fmtlib/fmt/releases', 195 headers=auth_headers, 196 data=json.dumps({'tag_name': version, 197 'target_commitish': 'release', 198 'body': changes, 'draft': True})) 199 if r.status_code != 201: 200 raise Exception('Failed to create a release ' + str(r)) 201 id = r.json()['id'] 202 uploads_url = 'https://uploads.github.com/repos/fmtlib/fmt/releases' 203 package = 'fmt-{}.zip'.format(version) 204 r = requests.post( 205 '{}/{}/assets?name={}'.format(uploads_url, id, package), 206 headers={'Content-Type': 'application/zip'} | auth_headers, 207 data=open('build/fmt/' + package, 'rb')) 208 if r.status_code != 201: 209 raise Exception('Failed to upload an asset ' + str(r)) 210 211 update_site(env) 212 213if __name__ == '__main__': 214 args = docopt.docopt(__doc__) 215 if args.get('release'): 216 release(args) 217 elif args.get('site'): 218 update_site(create_build_env()) 219