manage.py 9.02 KB
Newer Older
1 2 3 4 5
#!/usr/bin/env python

"""Manage site and releases.

Usage:
6 7 8
  manage.py release [<branch>]  Create a new release. $FTM_TOKEN should contain
                                a GitHub personal access token obtained from
                                https://github.com/settings/tokens.
9 10 11 12
  manage.py site
"""

from __future__ import print_function
Victor Zverovich's avatar
Victor Zverovich committed
13
import datetime, docopt, errno, fileinput, json, os
14
import re, requests, shutil, sys, tempfile
Victor Zverovich's avatar
Victor Zverovich committed
15
from contextlib import contextmanager
16 17 18 19 20 21 22 23 24 25 26
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)

Victor Zverovich's avatar
Victor Zverovich committed
27 28
    def add(self, *args):
        return self.call('add', args, cwd=self.dir)
29 30 31 32 33 34 35

    def checkout(self, *args):
        return self.call('checkout', args, cwd=self.dir)

    def clean(self, *args):
        return self.call('clean', args, cwd=self.dir)

Victor Zverovich's avatar
Victor Zverovich committed
36 37 38 39 40
    def clone(self, *args):
        return self.call('clone', list(args) + [self.dir])

    def commit(self, *args):
        return self.call('commit', args, cwd=self.dir)
41 42 43 44

    def pull(self, *args):
        return self.call('pull', args, cwd=self.dir)

Victor Zverovich's avatar
Victor Zverovich committed
45 46 47 48 49 50
    def push(self, *args):
        return self.call('push', args, cwd=self.dir)

    def reset(self, *args):
        return self.call('reset', args, cwd=self.dir)

51
    def update(self, *args):
Victor Zverovich's avatar
Victor Zverovich committed
52 53
        clone = not os.path.exists(self.dir)
        if clone:
54
            self.clone(*args)
Victor Zverovich's avatar
Victor Zverovich committed
55 56 57 58 59 60 61
        return clone


def clean_checkout(repo, branch):
    repo.clean('-f', '-d')
    repo.reset('--hard')
    repo.checkout(branch)
62 63 64


class Runner:
Victor Zverovich's avatar
Victor Zverovich committed
65 66
    def __init__(self, cwd):
        self.cwd = cwd
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84

    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'
Victor Zverovich's avatar
Victor Zverovich committed
85
    env.versions = build.versions
86 87 88 89 90 91 92 93

    # 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


Victor Zverovich's avatar
Victor Zverovich committed
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
@contextmanager
def rewrite(filename):
    class Buffer:
        pass
    buffer = Buffer()
    if not os.path.exists(filename):
        buffer.data = ''
        yield buffer
        return
    with open(filename) as f:
        buffer.data = f.read()
    yield buffer
    with open(filename, 'w') as f:
        f.write(buffer.data)


110 111 112 113 114 115 116 117 118
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')

Victor Zverovich's avatar
Victor Zverovich committed
119
    for version in env.versions:
Victor Zverovich's avatar
Victor Zverovich committed
120
        clean_checkout(env.fmt_repo, version)
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
        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']:
            pattern = re.compile('doxygenfunction.. (bin|oct|hexu|hex)$', re.M)
Victor Zverovich's avatar
Victor Zverovich committed
141 142 143 144 145
            with rewrite(os.path.join(target_doc_dir, filename)) as b:
                b.data = b.data.replace('std::ostream &', 'std::ostream&')
                b.data = re.sub(pattern, r'doxygenfunction:: \1(int)', b.data)
                b.data = b.data.replace('std::FILE*', 'std::FILE *')
                b.data = b.data.replace('unsigned int', 'unsigned')
Victor Zverovich's avatar
Victor Zverovich committed
146
                b.data = b.data.replace('operator""_', 'operator"" _')
147
                b.data = b.data.replace(', size_t', ', std::size_t')
Victor Zverovich's avatar
Victor Zverovich committed
148 149 150
        # Fix a broken link in index.rst.
        index = os.path.join(target_doc_dir, 'index.rst')
        with rewrite(index) as b:
Victor Zverovich's avatar
Victor Zverovich committed
151 152
            b.data = b.data.replace(
                'doc/latest/index.html#format-string-syntax', 'syntax.html')
153 154 155 156 157
        # 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
Victor Zverovich's avatar
Victor Zverovich committed
158 159 160
        if LooseVersion(version) >= LooseVersion('5.0.0'):
            include_dir = os.path.join(include_dir, 'include', 'fmt')
        elif LooseVersion(version) >= LooseVersion('3.0.0'):
161 162 163 164 165 166 167 168 169 170 171 172 173 174
            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)
Victor Zverovich's avatar
Victor Zverovich committed
175 176 177 178 179
        try:
            shutil.rmtree(version_doc_dir)
        except OSError as e:
            if e.errno != errno.ENOENT:
                raise
180 181 182 183 184
        shutil.move(html_dir, version_doc_dir)


def release(args):
    env = create_build_env()
Victor Zverovich's avatar
Victor Zverovich committed
185
    fmt_repo = env.fmt_repo
186 187 188 189

    branch = args.get('<branch>')
    if branch is None:
        branch = 'master'
Victor Zverovich's avatar
Victor Zverovich committed
190 191
    if not fmt_repo.update('-b', branch, fmt_repo_url):
        clean_checkout(fmt_repo, branch)
192 193 194 195

    # Convert changelog from RST to GitHub-flavored Markdown and get the
    # version.
    changelog = 'ChangeLog.rst'
Victor Zverovich's avatar
Victor Zverovich committed
196
    changelog_path = os.path.join(fmt_repo.dir, changelog)
197 198 199
    import rst2md
    changes, version = rst2md.convert(changelog_path)
    cmakelists = 'CMakeLists.txt'
Victor Zverovich's avatar
Victor Zverovich committed
200
    for line in fileinput.input(os.path.join(fmt_repo.dir, cmakelists),
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
                                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)
218

Victor Zverovich's avatar
Victor Zverovich committed
219 220
    # Add the version to the build script.
    script = os.path.join('doc', 'build.py')
221 222
    script_path = os.path.join(fmt_repo.dir, script)
    for line in fileinput.input(script_path, inplace=True):
Victor Zverovich's avatar
Victor Zverovich committed
223
      m = re.match(r'( *versions = )\[(.+)\]', line)
224
      if m:
Victor Zverovich's avatar
Victor Zverovich committed
225
        line = '{}[{}, \'{}\']\n'.format(m.group(1), m.group(2), version)
226 227
      sys.stdout.write(line)

Victor Zverovich's avatar
Victor Zverovich committed
228
    fmt_repo.checkout('-B', 'release')
229
    fmt_repo.add(changelog, cmakelists, script)
Victor Zverovich's avatar
Victor Zverovich committed
230
    fmt_repo.commit('-m', 'Update version')
231 232

    # Build the docs and package.
Victor Zverovich's avatar
Victor Zverovich committed
233
    run = Runner(fmt_repo.dir)
234 235 236 237 238
    run('cmake', '.')
    run('make', 'doc', 'package_source')
    update_site(env)

    # Create a release on GitHub.
Victor Zverovich's avatar
Victor Zverovich committed
239
    fmt_repo.push('origin', 'release')
Victor Zverovich's avatar
Victor Zverovich committed
240
    params = {'access_token': os.getenv('FMT_TOKEN')}
241
    r = requests.post('https://api.github.com/repos/fmtlib/fmt/releases',
Victor Zverovich's avatar
Victor Zverovich committed
242
                      params=params,
243 244 245 246 247
                      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))
Victor Zverovich's avatar
Victor Zverovich committed
248 249 250
    id = r.json()['id']
    uploads_url = 'https://uploads.github.com/repos/fmtlib/fmt/releases'
    package = 'fmt-{}.zip'.format(version)
251 252
    r = requests.post(
        '{}/{}/assets?name={}'.format(uploads_url, id, package),
253 254
        headers={'Content-Type': 'application/zip'},
        params=params, data=open('build/fmt/' + package, 'rb'))
255 256
    if r.status_code != 201:
        raise Exception('Failed to upload an asset ' + str(r))
257 258 259 260 261 262 263 264


if __name__ == '__main__':
    args = docopt.docopt(__doc__)
    if args.get('release'):
        release(args)
    elif args.get('site'):
        update_site(create_build_env())