1# -*- coding: utf-8 -*-
2# Copyright 2010-2018, Google Inc.
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are
7# met:
8#
9#     * Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11#     * Redistributions in binary form must reproduce the above
12# copyright notice, this list of conditions and the following disclaimer
13# in the documentation and/or other materials provided with the
14# distribution.
15#     * Neither the name of Google Inc. nor the names of its
16# contributors may be used to endorse or promote products derived from
17# this software without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31"""A library to operate version definition file.
32
33This script has two functionarity which relate to version definition file.
34
351. Generate version definition file from template and given parameters.
36  To generate version definition file, use GenerateVersionFileFromTemplate
37  method.
38
392. Parse (generated) version definition file.
40  To parse, use MozcVersion class.
41
42Typically version definition file is ${PROJECT_ROOT}/mozc_version.txt
43(Not in the repository because it is generated by this script)
44Typically version template file is
45${PROJECT_ROOT}/data/version/mozc_version_template.bzl,
46which is in the repository.
47The syntax of template is written in the template file.
48"""
49# TODO(matsuzaki): MozcVersion class should have factory method which takes
50#   file path and we should remove all the module methods instead to
51#   simplify the design. Currently I'd keep this design to reduce
52#   client side's change.
53
54import datetime
55import logging
56import optparse
57import os
58import re
59import sys
60
61
62TARGET_PLATFORM_TO_DIGIT = {
63    'Windows': '0',
64    'Mac': '1',
65    'Linux': '2',
66    'Android': '3',
67    'NaCl': '4',
68    }
69
70VERSION_PROPERTIES = [
71    'MAJOR',
72    'MINOR',
73    'BUILD',
74    'REVISION',
75    'ANDROID_VERSION_CODE',
76    'TARGET_PLATFORM',
77    'QT_VERSION',
78    'ANDROID_APPLICATION_ID',
79    'ANDROID_SERVICE_NAME',
80    'ANDROID_ARCH',
81    'ENGINE_VERSION',
82    # 'DATA_VERSION' is not included as it's the property of data file to be
83    # independent of executables.
84    ]
85
86MOZC_EPOCH = datetime.date(2009, 5, 24)
87
88
89def _GetRevisionForPlatform(revision, target_platform):
90  """Returns the revision for the current platform."""
91  if revision is None:
92    logging.critical('REVISION property is not found in the template file')
93    sys.exit(1)
94  last_digit = TARGET_PLATFORM_TO_DIGIT.get(target_platform, None)
95  if last_digit is None:
96    logging.critical('target_platform %s is invalid. Accetable ones are %s',
97                     target_platform, list(TARGET_PLATFORM_TO_DIGIT.keys()))
98    sys.exit(1)
99
100  if not revision:
101    return revision
102
103  if last_digit:
104    return revision[0:-1] + last_digit
105
106  # If not supported, just use the specified version.
107  return revision
108
109
110def _ParseVersionTemplateFile(template_path, target_platform,
111                              android_application_id, android_arch,
112                              qt_version):
113  """Parses a version definition file.
114
115  Args:
116    template_path: A filename which has the version definition.
117    target_platform: The target platform on which the programs run.
118    android_application_id: Android application id.
119    android_arch: Android architecture (arm, x86, mips)
120    qt_version: '4' for Qt4, '5' for Qt5, and '' or None for no-Qt.
121  Returns:
122    A dictionary generated from the template file.
123  """
124  template_dict = {}
125  with open(template_path) as template_file:
126    for line in template_file:
127      matchobj = re.match(r'(\w+)=(.*)', line.strip())
128      if matchobj:
129        var = matchobj.group(1)
130        val = matchobj.group(2)
131        if var in template_dict:
132          logging.warning(('Dupulicate key: "%s". Later definition "%s"'
133                           'overrides earlier one "%s".'),
134                          var, val, template_dict[var])
135        template_dict[var] = val
136
137  # Some properties need to be tweaked.
138  template_dict['REVISION'] = _GetRevisionForPlatform(
139      template_dict.get('REVISION', None), target_platform)
140
141  template_dict['ANDROID_VERSION_CODE'] = (
142      str(_GetAndroidVersionCode(int(template_dict['BUILD']), android_arch)))
143
144  template_dict['TARGET_PLATFORM'] = target_platform
145  template_dict['QT_VERSION'] = qt_version
146  template_dict['ANDROID_APPLICATION_ID'] = android_application_id
147  template_dict['ANDROID_SERVICE_NAME'] = (
148      'org.mozc.android.inputmethod.japanese.MozcService')
149  template_dict['ANDROID_ARCH'] = android_arch
150  return template_dict
151
152
153def _GetAndroidVersionCode(base_version_code, arch):
154  """Gets version code based on base version code and architecture.
155
156  Args:
157    base_version_code: is typically equal to the field BUILD in mozc_version.txt
158    arch: Android's architecture (e.g., x86, arm, mips)
159
160  Returns:
161    version code (int)
162
163  Raises:
164    RuntimeError: arch is unexpected one or base_version_code is too big.
165
166  Version code format:
167   0006BBBBBA
168   A: ABI (0: Fat, 6: x86_64, 5:arm64, 4:mips64, 3: x86, 2: armeabi-v7a, 1:mips)
169   B: ANDROID_VERSION_CODE
170
171  Note:
172  - Prefix 6 is introduced because of historical reason.
173    Previously ANDROID_VERSION_CODE (B) was placed after ABI (A) but
174    it's found that swpping the order is reasonable.
175    Previously version code for x86 was always greater than that for armeabi.
176    Therefore version-check rule like "Version code of update must be greater
177    than that of previous" cannot be introduced.
178  """
179  arch_to_abi_code = {
180      'x86_64': 6,
181      'arm64': 5,
182      'mips64': 4,
183      'x86': 3,
184      'arm': 2,
185      'mips': 1,
186  }
187  abi_code = arch_to_abi_code.get(arch)
188  if abi_code is None:
189    raise RuntimeError('Unexpected architecture; %s' % arch)
190  if base_version_code >= 10000:
191    raise RuntimeError('Version code is greater than 10000. '
192                       'It is time to revisit version code scheme.')
193  return int('6%05d%d' % (base_version_code, abi_code))
194
195
196def _GetVersionInFormat(properties, version_format):
197  """Returns the version string based on the specified format.
198
199  format can contains @MAJOR@, @MINOR@, @BUILD@ and @REVISION@ which are
200  replaced by self._major, self._minor, self._build, and self._revision
201  respectively.
202
203  Args:
204    properties: a property dicitonary. Typically gotten from
205      _ParseVersionTemplateFile method.
206    version_format: a string which contains version patterns.
207
208  Returns:
209    Return the version string in the format of format.
210  """
211
212  result = version_format
213  for keyword in VERSION_PROPERTIES:
214    result = result.replace('@%s@' % keyword, properties.get(keyword, ''))
215  return result
216
217
218def GenerateVersionFileFromTemplate(template_path,
219                                    output_path,
220                                    version_format,
221                                    target_platform,
222                                    android_application_id='',
223                                    android_arch='arm',
224                                    qt_version=''):
225  """Generates version file from template file and given parameters.
226
227  Args:
228    template_path: A path to template file.
229    output_path: A path to generated version file.
230      If already exists and the content will not be updated, nothing is done
231      (the timestamp is not updated).
232    version_format: A string which contans version patterns.
233    target_platform: The target platform on which the programs run.
234    android_application_id: Android application id.
235    android_arch: Android architecture (arm, x86, mips)
236    qt_version: '4' for Qt4, '5' for Qt5, and '' or None for no-Qt.
237  """
238
239  properties = _ParseVersionTemplateFile(template_path, target_platform,
240                                         android_application_id,
241                                         android_arch, qt_version)
242  version_definition = _GetVersionInFormat(properties, version_format)
243  old_content = ''
244  if os.path.exists(output_path):
245    # If the target file already exists, need to check the necessity of update
246    # to reduce file-creation frequency.
247    # Currently generated version file is not seen from Make (and Make like
248    # tools) so recreation will not cause serious issue but just in case.
249    with open(output_path) as output_file:
250      old_content = output_file.read()
251
252  if version_definition != old_content:
253    with open(output_path, 'w') as output_file:
254      output_file.write(version_definition)
255
256
257def GenerateVersionFile(version_template_path, version_path, target_platform,
258                        android_application_id, android_arch, qt_version):
259  """Reads the version template file and stores it into version_path.
260
261  This doesn't update the "version_path" if nothing will be changed to
262  reduce unnecessary build caused by file timestamp.
263
264  Args:
265    version_template_path: a file name which contains the template of version.
266    version_path: a file name to be stored the official version.
267    target_platform: target platform name. c.f. --target_platform option
268    android_application_id: [Android Only] application id
269      (e.g. org.mozc.android).
270    android_arch: Android architecture (arm, x86, mips)
271    qt_version: '4' for Qt4, '5' for Qt5, and '' or None for no-Qt.
272  """
273  version_format = '\n'.join([
274      'MAJOR=@MAJOR@',
275      'MINOR=@MINOR@',
276      'BUILD=@BUILD@',
277      'REVISION=@REVISION@',
278      'ANDROID_VERSION_CODE=@ANDROID_VERSION_CODE@',
279      'TARGET_PLATFORM=@TARGET_PLATFORM@',
280      'QT_VERSION=@QT_VERSION@',
281      'ANDROID_APPLICATION_ID=@ANDROID_APPLICATION_ID@',
282      'ANDROID_SERVICE_NAME=@ANDROID_SERVICE_NAME@',
283      'ANDROID_ARCH=@ANDROID_ARCH@',
284      'ENGINE_VERSION=@ENGINE_VERSION@',
285  ]) + '\n'
286  GenerateVersionFileFromTemplate(
287      version_template_path,
288      version_path,
289      version_format,
290      target_platform=target_platform,
291      android_application_id=android_application_id,
292      android_arch=android_arch,
293      qt_version=qt_version)
294
295
296class MozcVersion(object):
297  """A class to parse and maintain the version definition data.
298
299  Note that this class is not intended to parse "template" file but to
300  "generated" file.
301  Typical usage is;
302    GenerateVersionFileFromTemplate(template_path, version_path, format)
303    version = MozcVersion(version_path)
304  """
305
306  def __init__(self, path):
307    """Parses a version definition file.
308
309    Args:
310      path: A filename which has the version definition.
311            If the file is not existent, empty properties are prepared instead.
312    """
313
314    self._properties = {}
315    if not os.path.isfile(path):
316      return
317    with open(path) as file:
318      for line in file:
319        matchobj = re.match(r'(\w+)=(.*)', line.strip())
320        if matchobj:
321          var = matchobj.group(1)
322          val = matchobj.group(2)
323          if var not in self._properties:
324            self._properties[var] = val
325
326    # Check mandatory properties.
327    for key in VERSION_PROPERTIES:
328      if key not in self._properties:
329        # Don't raise error nor exit.
330        # Error handling is the client's responsibility.
331        logging.warning('Mandatory key "%s" does not exist in %s', key, path)
332
333  def IsDevChannel(self):
334    """Returns true if the parsed version is dev-channel."""
335    revision = self._properties['REVISION']
336    return revision is not None and len(revision) >= 3 and revision[-3] == '1'
337
338  def GetTargetPlatform(self):
339    """Returns the target platform.
340
341    Returns:
342      A string for target platform.
343      If the version file is not existent, None is returned.
344    """
345    return self._properties.get('TARGET_PLATFORM', None)
346
347  def GetQtVersion(self):
348    """Returns the target Qt version.
349
350    Returns:
351      A string that indicates the Qt version.
352      '4' for Qt4, '5' for Qt5, and '' for no-Qt.
353    """
354    return self._properties.get('QT_VERSION', None)
355
356  def GetVersionString(self):
357    """Returns the normal version info string.
358
359    Returns:
360      a string in format of "MAJOR.MINOR.BUILD.REVISION"
361    """
362    return self.GetVersionInFormat('@MAJOR@.@MINOR@.@BUILD@.@REVISION@')
363
364  def GetVersionInFormat(self, version_format):
365    """Returns the version string based on the specified format."""
366    return _GetVersionInFormat(self._properties, version_format)
367
368  def GetAndroidArch(self):
369    """Returns Android architecture."""
370    return self._properties.get('ANDROID_ARCH', None)
371
372
373def main():
374  """Generates version file based on the default format.
375
376  Generated file is mozc_version.txt compatible.
377  """
378  parser = optparse.OptionParser(usage='Usage: %prog ')
379  parser.add_option('--template_path', dest='template_path',
380                    help='Path to a template version file.')
381  parser.add_option('--output', dest='output',
382                    help='Path to the output version file.')
383  parser.add_option('--target_platform', dest='target_platform',
384                    help='Target platform of the version info.')
385  parser.add_option('--android_application_id', dest='android_application_id',
386                    default='my.application.id',
387                    help='Specifies the application id (Android Only).')
388  parser.add_option('--android_arch', dest='android_arch',
389                    default='arm',
390                    help='Specifies Android architecture (arm, x86, mips) '
391                    '(Android Only)')
392  parser.add_option('--qtver', dest='qtver', choices=('4', '5', ''),
393                    default='', help='Specifies Qt version (desktop only)')
394  (options, args) = parser.parse_args()
395  assert not args, 'Unexpected arguments.'
396  assert options.template_path, 'No --template_path was specified.'
397  assert options.output, 'No --output was specified.'
398  assert options.target_platform, 'No --target_platform was specified.'
399
400  GenerateVersionFile(
401      version_template_path=options.template_path,
402      version_path=options.output,
403      target_platform=options.target_platform,
404      android_application_id=options.android_application_id,
405      android_arch=options.android_arch,
406      qt_version=options.qtver)
407
408if __name__ == '__main__':
409  main()
410