summaryrefslogtreecommitdiffstats
path: root/utils
diff options
context:
space:
mode:
Diffstat (limited to 'utils')
-rw-r--r--utils/CHANGES_template20
-rw-r--r--utils/__init__.py0
-rw-r--r--utils/babel_runner.py171
-rw-r--r--utils/bump_docker.sh16
-rw-r--r--utils/bump_version.py183
-rw-r--r--utils/release-checklist108
6 files changed, 498 insertions, 0 deletions
diff --git a/utils/CHANGES_template b/utils/CHANGES_template
new file mode 100644
index 0000000..991925c
--- /dev/null
+++ b/utils/CHANGES_template
@@ -0,0 +1,20 @@
+Release x.y.z (in development)
+==============================
+
+Dependencies
+------------
+
+Incompatible changes
+--------------------
+
+Deprecated
+----------
+
+Features added
+--------------
+
+Bugs fixed
+----------
+
+Testing
+--------
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/__init__.py
diff --git a/utils/babel_runner.py b/utils/babel_runner.py
new file mode 100644
index 0000000..32e6fe6
--- /dev/null
+++ b/utils/babel_runner.py
@@ -0,0 +1,171 @@
+"""Run babel for translations.
+
+Usage:
+
+babel_runner.py extract
+ Extract messages from the source code and update the ".pot" template file.
+
+babel_runner.py update
+ Update all language catalogues in "sphinx/locale/<language>/LC_MESSAGES"
+ with the current messages in the template file.
+
+babel_runner.py compile
+ Compile the ".po" catalogue files to ".mo" and ".js" files.
+"""
+
+import json
+import logging
+import os
+import sys
+
+from babel.messages.frontend import compile_catalog, extract_messages, update_catalog
+from babel.messages.pofile import read_po
+
+import sphinx
+
+ROOT = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", ".."))
+
+
+class compile_catalog_plusjs(compile_catalog):
+ """
+ An extended command that writes all message strings that occur in
+ JavaScript files to a JavaScript file along with the .mo file.
+
+ Unfortunately, babel's setup command isn't built very extensible, so
+ most of the run() code is duplicated here.
+ """
+
+ def run(self):
+ if super().run():
+ print("Compiling failed.", file=sys.stderr)
+ raise SystemExit(2)
+
+ for domain in self.domain:
+ self._run_domain_js(domain)
+
+ def _run_domain_js(self, domain):
+ po_files = []
+ js_files = []
+
+ if not self.input_file:
+ if self.locale:
+ po_files.append((self.locale,
+ os.path.join(self.directory, self.locale,
+ 'LC_MESSAGES',
+ domain + '.po')))
+ js_files.append(os.path.join(self.directory, self.locale,
+ 'LC_MESSAGES',
+ domain + '.js'))
+ else:
+ for locale in os.listdir(self.directory):
+ po_file = os.path.join(self.directory, locale,
+ 'LC_MESSAGES',
+ domain + '.po')
+ if os.path.exists(po_file):
+ po_files.append((locale, po_file))
+ js_files.append(os.path.join(self.directory, locale,
+ 'LC_MESSAGES',
+ domain + '.js'))
+ else:
+ po_files.append((self.locale, self.input_file))
+ if self.output_file:
+ js_files.append(self.output_file)
+ else:
+ js_files.append(os.path.join(self.directory, self.locale,
+ 'LC_MESSAGES',
+ domain + '.js'))
+
+ for js_file, (locale, po_file) in zip(js_files, po_files):
+ with open(po_file, encoding='utf8') as infile:
+ catalog = read_po(infile, locale)
+
+ if catalog.fuzzy and not self.use_fuzzy:
+ continue
+
+ self.log.info('writing JavaScript strings in catalog %s to %s',
+ po_file, js_file)
+
+ jscatalog = {}
+ for message in catalog:
+ if any(x[0].endswith(('.js', '.js_t', '.html'))
+ for x in message.locations):
+ msgid = message.id
+ if isinstance(msgid, (list, tuple)):
+ msgid = msgid[0]
+ jscatalog[msgid] = message.string
+
+ obj = json.dumps({
+ 'messages': jscatalog,
+ 'plural_expr': catalog.plural_expr,
+ 'locale': f'{catalog.locale!s}'
+ }, sort_keys=True, indent=4)
+ with open(js_file, 'w', encoding='utf8') as outfile:
+ outfile.write(f'Documentation.addTranslations({obj});')
+
+
+def _get_logger():
+ log = logging.getLogger('babel')
+ log.setLevel(logging.INFO)
+ handler = logging.StreamHandler()
+ handler.setFormatter(logging.Formatter('%(message)s'))
+ log.addHandler(handler)
+ return log
+
+
+def run_extract():
+ os.chdir(ROOT)
+ command = extract_messages()
+ command.log = _get_logger()
+ command.initialize_options()
+
+ command.keywords = "_ __ l_ lazy_gettext"
+ command.mapping_file = "babel.cfg"
+ command.output_file = os.path.join("sphinx", "locale", "sphinx.pot")
+ command.project = "Sphinx"
+ command.version = sphinx.__version__
+ command.input_paths = "sphinx"
+
+ command.finalize_options()
+ return command.run()
+
+
+def run_update():
+ os.chdir(ROOT)
+ command = update_catalog()
+ command.log = _get_logger()
+ command.initialize_options()
+
+ command.domain = "sphinx"
+ command.input_file = os.path.join("sphinx", "locale", "sphinx.pot")
+ command.output_dir = os.path.join("sphinx", "locale")
+
+ command.finalize_options()
+ return command.run()
+
+
+def run_compile():
+ os.chdir(ROOT)
+ command = compile_catalog_plusjs()
+ command.log = _get_logger()
+ command.initialize_options()
+
+ command.domain = "sphinx"
+ command.directory = os.path.join("sphinx", "locale")
+
+ command.finalize_options()
+ return command.run()
+
+
+if __name__ == '__main__':
+ try:
+ action = sys.argv[1].lower()
+ except IndexError:
+ print(__doc__, file=sys.stderr)
+ raise SystemExit(2)
+
+ if action == "extract":
+ raise SystemExit(run_extract())
+ if action == "update":
+ raise SystemExit(run_update())
+ if action == "compile":
+ raise SystemExit(run_compile())
diff --git a/utils/bump_docker.sh b/utils/bump_docker.sh
new file mode 100644
index 0000000..436aaef
--- /dev/null
+++ b/utils/bump_docker.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+set -ex
+
+if [ -z "$1" ]; then
+ echo "bump_docker.sh [VERSION]"
+ exit
+fi
+
+cd docker
+git checkout .
+sed -i "" -e "s/==[0-9.]\{1,\}/==$1/" base/Dockerfile
+sed -i "" -e "s/==[0-9.]\{1,\}/==$1/" latexpdf/Dockerfile
+git commit -am "Bump to $1"
+git tag $1
+git push origin master --tags
diff --git a/utils/bump_version.py b/utils/bump_version.py
new file mode 100644
index 0000000..e2aefa8
--- /dev/null
+++ b/utils/bump_version.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+
+import argparse
+import os
+import re
+import sys
+from contextlib import contextmanager
+from datetime import datetime
+
+script_dir = os.path.dirname(__file__)
+package_dir = os.path.abspath(os.path.join(script_dir, '..'))
+
+RELEASE_TYPE = {'a': 'alpha', 'b': 'beta'}
+
+
+def stringify_version(version_info, in_develop=True):
+ version = '.'.join(str(v) for v in version_info[:3])
+ if not in_develop and version_info[3] != 'final':
+ version += version_info[3][0] + str(version_info[4])
+
+ return version
+
+
+def bump_version(path, version_info, in_develop=True):
+ version = stringify_version(version_info, in_develop)
+
+ with open(path, 'r', encoding='utf-8') as f:
+ lines = f.read().splitlines()
+
+ for i, line in enumerate(lines):
+ if line.startswith('__version__ = '):
+ lines[i] = f"__version__ = '{version}'"
+ continue
+ if line.startswith('version_info = '):
+ lines[i] = f'version_info = {version_info}'
+ continue
+ if line.startswith('_in_development = '):
+ lines[i] = f'_in_development = {in_develop}'
+ continue
+
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write('\n'.join(lines) + '\n')
+
+
+def parse_version(version):
+ matched = re.search(r'^(\d+)\.(\d+)$', version)
+ if matched:
+ major, minor = matched.groups()
+ return (int(major), int(minor), 0, 'final', 0)
+
+ matched = re.search(r'^(\d+)\.(\d+)\.(\d+)$', version)
+ if matched:
+ major, minor, rev = matched.groups()
+ return (int(major), int(minor), int(rev), 'final', 0)
+
+ matched = re.search(r'^(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version)
+ if matched:
+ major, minor, typ, relver = matched.groups()
+ release = RELEASE_TYPE.get(typ, typ)
+ return (int(major), int(minor), 0, release, int(relver))
+
+ matched = re.search(r'^(\d+)\.(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version)
+ if matched:
+ major, minor, rev, typ, relver = matched.groups()
+ release = RELEASE_TYPE.get(typ, typ)
+ return (int(major), int(minor), int(rev), release, int(relver))
+
+ raise RuntimeError('Unknown version: %s' % version)
+
+
+class Skip(Exception):
+ pass
+
+
+@contextmanager
+def processing(message):
+ try:
+ print(message + ' ... ', end='')
+ yield
+ except Skip as exc:
+ print('skip: %s' % exc)
+ except Exception:
+ print('error')
+ raise
+ else:
+ print('done')
+
+
+class Changes:
+ def __init__(self, path):
+ self.path = path
+ self.fetch_version()
+
+ def fetch_version(self):
+ with open(self.path, encoding='utf-8') as f:
+ version = f.readline().strip()
+ matched = re.search(r'^Release (.*) \((.*)\)$', version)
+ if matched is None:
+ raise RuntimeError('Unknown CHANGES format: %s' % version)
+
+ self.version, self.release_date = matched.groups()
+ self.version_info = parse_version(self.version)
+ if self.release_date == 'in development':
+ self.in_development = True
+ else:
+ self.in_development = False
+
+ def finalize_release_date(self):
+ release_date = datetime.now().strftime('%b %d, %Y')
+ heading = 'Release %s (released %s)' % (self.version, release_date)
+
+ with open(self.path, 'r+', encoding='utf-8') as f:
+ f.readline() # skip first two lines
+ f.readline()
+ body = f.read()
+
+ f.seek(0)
+ f.truncate(0)
+ f.write(heading + '\n')
+ f.write('=' * len(heading) + '\n')
+ f.write(self.filter_empty_sections(body))
+
+ def add_release(self, version_info):
+ if version_info[-2:] in (('beta', 0), ('final', 0)):
+ version = stringify_version(version_info)
+ else:
+ reltype = version_info[3]
+ version = '%s %s%s' % (stringify_version(version_info),
+ RELEASE_TYPE.get(reltype, reltype),
+ version_info[4] or '')
+ heading = 'Release %s (in development)' % version
+
+ with open(os.path.join(script_dir, 'CHANGES_template'), encoding='utf-8') as f:
+ f.readline() # skip first two lines
+ f.readline()
+ tmpl = f.read()
+
+ with open(self.path, 'r+', encoding='utf-8') as f:
+ body = f.read()
+
+ f.seek(0)
+ f.truncate(0)
+ f.write(heading + '\n')
+ f.write('=' * len(heading) + '\n')
+ f.write(tmpl)
+ f.write('\n')
+ f.write(body)
+
+ def filter_empty_sections(self, body):
+ return re.sub('^\n.+\n-{3,}\n+(?=\n.+\n[-=]{3,}\n)', '', body, flags=re.M)
+
+
+def parse_options(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument('version', help='A version number (cf. 1.6b0)')
+ parser.add_argument('--in-develop', action='store_true')
+ options = parser.parse_args(argv)
+ options.version = parse_version(options.version)
+ return options
+
+
+def main():
+ options = parse_options(sys.argv[1:])
+
+ with processing("Rewriting sphinx/__init__.py"):
+ bump_version(os.path.join(package_dir, 'sphinx/__init__.py'),
+ options.version, options.in_develop)
+
+ with processing('Rewriting CHANGES'):
+ changes = Changes(os.path.join(package_dir, 'CHANGES'))
+ if changes.version_info == options.version:
+ if changes.in_development:
+ changes.finalize_release_date()
+ else:
+ raise Skip('version not changed')
+ else:
+ if changes.in_development:
+ print('WARNING: last version is not released yet: %s' % changes.version)
+ changes.add_release(options.version)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/utils/release-checklist b/utils/release-checklist
new file mode 100644
index 0000000..febbbae
--- /dev/null
+++ b/utils/release-checklist
@@ -0,0 +1,108 @@
+Release checklist
+=================
+
+for stable releases
+-------------------
+
+* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.Y.x" and all tests has passed
+* Run ``git fetch; git status`` and check nothing changed
+* ``python utils/bump_version.py X.Y.Z``
+* Check diff by ``git diff``
+* ``git commit -am 'Bump to X.Y.Z final'``
+* ``make clean``
+* ``python -m build .``
+* ``twine upload dist/Sphinx-* --sign --identity [your GPG key]``
+* open https://pypi.org/project/Sphinx/ and check there are no obvious errors
+* ``sh utils/bump_docker.sh X.Y.Z``
+* ``git tag vX.Y.Z -m "Sphinx X.Y.Z"``
+* ``python utils/bump_version.py --in-develop X.Y.Zb0`` (ex. 1.5.3b0)
+* Check diff by ``git diff``
+* ``git commit -am 'Bump version'``
+* ``git push origin X.Y.x --tags``
+* ``git checkout X.x``
+* ``git merge X.Y.x``
+* ``git push origin X.x``
+* Add new version/milestone to tracker categories
+* Write announcement and send to sphinx-dev, sphinx-users and python-announce
+
+for first beta releases
+-----------------------
+
+* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:master" and all tests has passed
+* Run ``git fetch; git status`` and check nothing changed
+* ``python utils/bump_version.py X.Y.0b1``
+* Check diff by ``git diff``
+* ``git commit -am 'Bump to X.Y.0 beta1'``
+* ``make clean``
+* ``python -m build .``
+* ``twine upload dist/Sphinx-* --sign --identity [your GPG key]``
+* open https://pypi.org/project/Sphinx/ and check there are no obvious errors
+* ``git tag vX.Y.0b1 -m "Sphinx X.Y.0b1"``
+* ``python utils/bump_version.py --in-develop X.Y.0b2`` (ex. 1.6.0b2)
+* Check diff by ``git diff``
+* ``git commit -am 'Bump version'``
+* ``git checkout -b X.x``
+* ``git push origin X.x --tags``
+* ``git checkout master``
+* ``git merge X.x``
+* ``python utils/bump_version.py --in-develop A.B.0b0`` (ex. 1.7.0b0)
+* Check diff by ``git diff``
+* ``git commit -am 'Bump version'``
+* ``git push origin master``
+* open https://github.com/sphinx-doc/sphinx/settings/branches and make ``X.Y`` branch protected
+* Add new version/milestone to tracker categories
+* Write announcement and send to sphinx-dev, sphinx-users and python-announce
+
+for other beta releases
+-----------------------
+
+* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.x" and all tests has passed
+* Run ``git fetch; git status`` and check nothing changed
+* ``python utils/bump_version.py X.Y.0bN``
+* Check diff by ``git diff``
+* ``git commit -am 'Bump to X.Y.0 betaN'``
+* ``make clean``
+* ``python -m build .``
+* ``twine upload dist/Sphinx-* --sign --identity [your GPG key]``
+* open https://pypi.org/project/Sphinx/ and check there are no obvious errors
+* ``git tag vX.Y.0bN -m "Sphinx X.Y.0bN"``
+* ``python utils/bump_version.py --in-develop X.Y.0bM`` (ex. 1.6.0b3)
+* Check diff by `git diff``
+* ``git commit -am 'Bump version'``
+* ``git push origin X.x --tags``
+* ``git checkout master``
+* ``git merge X.x``
+* ``git push origin master``
+* Add new version/milestone to tracker categories
+* Write announcement and send to sphinx-dev, sphinx-users and python-announce
+
+for major releases
+------------------
+
+* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.x" and all tests has passed
+* Run ``git fetch; git status`` and check nothing changed
+* Run ``git add sphinx``
+* Run ``git commit -am 'Update message catalogs'``
+* ``python utils/bump_version.py X.Y.0``
+* Check diff by ``git diff``
+* ``git commit -am 'Bump to X.Y.0 final'``
+* ``make clean``
+* ``python -m build .``
+* ``twine upload dist/Sphinx-* --sign --identity [your GPG key]``
+* open https://pypi.org/project/Sphinx/ and check there are no obvious errors
+* ``sh utils/bump_docker.sh X.Y.Z``
+* ``git tag vX.Y.0 -m "Sphinx X.Y.0"``
+* ``python utils/bump_version.py --in-develop X.Y.1b0`` (ex. 1.6.1b0)
+* Check diff by ``git diff``
+* ``git commit -am 'Bump version'``
+* ``git push origin X.x --tags``
+* ``git checkout master``
+* ``git merge X.x``
+* ``git push origin master``
+* open https://github.com/sphinx-doc/sphinx/settings/branches and make ``A.B`` branch *not* protected
+* ``git checkout A.B`` (checkout old stable)
+* Run ``git tag A.B -m "Sphinx A.B"`` to paste a tag instead branch
+* Run ``git push origin :A.B --tags`` to remove old stable branch
+* open https://readthedocs.org/dashboard/sphinx/versions/ and enable the released version
+* Add new version/milestone to tracker categories
+* Write announcement and send to sphinx-dev, sphinx-users and python-announce