xref: /aosp_15_r20/external/fmtlib/support/manage.py (revision 5c90c05cd622c0a81b57953a4d343e0e489f2e08)
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