manage.py 11 KB

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