noto-emoji/gen_version.py

193 lines
6.5 KiB
Python
Executable File

#!/usr/bin/env python3
#
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generate version string for NotoColorEmoji.
This parses the color emoji template file and updates the lines
containing version string info, writing a new file.
The nameID 5 field in the emoji font should reflect the commit/date
of the repo it was built from. This will build a string of the following
format:
Version 1.39;GOOG;noto-emoji:20170220:a8a215d2e889'
This is intended to indicate that it was built by Google from noto-emoji
at commit a8a215d2e889 and date 20170220 (since dates are a bit easier
to locate in time than commit hashes).
For building with external data we don't include the commit id as we
might be using different resources. Instead the version string is:
Version 1.39;GOOG;noto-emoji:20170518;BETA <msg>
Here the date is the current date, and the message after 'BETA ' is
provided using the '-b' flag. There's no commit hash. This also
bypasses some checks about the state of the repo.
The release number should have 2 or 3 minor digits. Right now we've been
using 2 but at the next major release we probably want to use 3. This
supports both. It will bump the version number if none is provided,
maintaining the minor digit length.
"""
import argparse
import datetime
import re
from nototools import tool_utils
# These are not very lenient, we expect to be applied to the noto color
# emoji template ttx file which matches these. Why then require the
# input argument, you ask? Um... testing?
_nameid_re = re.compile(r'\s*<namerecord nameID="5"')
_version_re = re.compile(r'\s*Version\s(\d+.\d{2,3})')
_headrev_re = re.compile(r'\s*<fontRevision value="(\d+.\d{2,3})"/>')
def _get_existing_version(lines):
"""Scan lines for all existing version numbers, and ensure they match.
Return the matched version number string."""
version = None
def check_version(new_version):
if version is not None and new_version != version:
raise Exception(
'version %s and namerecord version %s do not match' % (
version, new_version))
return new_version
saw_nameid = False
for line in lines:
if saw_nameid:
saw_nameid = False
m = _version_re.match(line)
if not m:
raise Exception('could not match line "%s" in namerecord' % line)
version = check_version(m.group(1))
elif _nameid_re.match(line):
saw_nameid = True
else:
m = _headrev_re.match(line)
if m:
version = check_version(m.group(1))
return version
def _version_to_mm(version):
majs, mins = version.split('.')
minor_len = len(mins)
return int(majs), int(mins), minor_len
def _mm_to_version(major, minor, minor_len):
fmt = '%%d.%%0%dd' % minor_len
return fmt % (major, minor)
def _version_compare(lhs, rhs):
lmaj, lmin, llen = _version_to_mm(lhs)
rmaj, rmin, rlen = _version_to_mm(rhs)
# if major versions differ, we don't care about the minor length, else
# they should be the same
if lmaj != rmaj:
return lmaj - rmaj
if llen != rlen:
raise Exception('minor version lengths differ: "%s" and "%s"' % (lhs, rhs))
return lmin - rmin
def _version_bump(version):
major, minor, minor_len = _version_to_mm(version)
minor = (minor + 1) % (10 ** minor_len)
if minor == 0:
raise Exception('cannot bump version "%s", requires new major' % version)
return _mm_to_version(major, minor, minor_len)
def _get_repo_version_str(beta):
"""See above for description of this string."""
if beta is not None:
date_str = datetime.date.today().strftime('%Y%m%d')
return 'GOOG;noto-emoji:%s;BETA %s' % (date_str, beta)
p = tool_utils.resolve_path('[emoji]')
commit, date, _ = tool_utils.git_head_commit(p)
if not tool_utils.git_check_remote_commit(p, commit):
raise Exception('emoji not on upstream master branch')
date_re = re.compile(r'(\d{4})-(\d{2})-(\d{2})')
m = date_re.match(date)
if not m:
raise Exception('could not match "%s" with "%s"' % (date, date_re.pattern))
ymd = ''.join(m.groups())
return 'GOOG;noto-emoji:%s:%s' % (ymd, commit[:12])
def _replace_existing_version(lines, version, version_str):
"""Update lines with new version strings in appropriate places."""
saw_nameid = False
for i in range(len(lines)):
line = lines[i]
if saw_nameid:
saw_nameid = False
# preserve indentation
lead_ws = len(line) - len(line.lstrip())
lines[i] = line[:lead_ws] + version_str + '\n'
elif _nameid_re.match(line):
saw_nameid = True
elif _headrev_re.match(line):
lead_ws = len(line) - len(line.lstrip())
lines[i] = line[:lead_ws] + '<fontRevision value="%s"/>\n' % version
def update_version(srcfile, dstfile, version, beta):
"""Update version in srcfile and write to dstfile. If version is None,
bumps the current version, else version must be greater than the
current version."""
with open(srcfile, 'r') as f:
lines = f.readlines()
current_version = _get_existing_version(lines)
if not version:
version = _version_bump(current_version)
elif version and _version_compare(version, current_version) <= 0:
raise Exception('new version %s is <= current version %s' % (
version, current_version))
version_str = 'Version %s;%s' % (version, _get_repo_version_str(beta))
_replace_existing_version(lines, version, version_str)
with open(dstfile, 'w') as f:
for line in lines:
f.write(line)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'-v', '--version', help='version number, default bumps the current '
'version', metavar='ver')
parser.add_argument(
'-s', '--src', help='ttx file with name and head tables',
metavar='file', required=True)
parser.add_argument(
'-d', '--dst', help='name of edited ttx file to write',
metavar='file', required=True)
parser.add_argument(
'-b', '--beta', help='beta tag if font is built using external resources')
args = parser.parse_args()
update_version(args.src, args.dst, args.version, args.beta)
if __name__ == '__main__':
main()