xref: /aosp_15_r20/external/angle/build/util/version.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1#!/usr/bin/env python3
2# Copyright 2014 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7version.py -- Chromium version string substitution utility.
8"""
9
10
11import argparse
12import os
13import stat
14import sys
15
16import android_chrome_version
17
18
19def FetchValuesFromFile(values_dict, file_name):
20  """
21  Fetches KEYWORD=VALUE settings from the specified file.
22
23  Everything to the left of the first '=' is the keyword,
24  everything to the right is the value.  No stripping of
25  white space, so beware.
26
27  The file must exist, otherwise you get the Python exception from open().
28  """
29  with open(file_name, 'r') as f:
30    for line in f.readlines():
31      key, val = line.rstrip('\r\n').split('=', 1)
32      values_dict[key] = val
33
34
35def FetchValues(file_list, is_official_build=None):
36  """
37  Returns a dictionary of values to be used for substitution.
38
39  Populates the dictionary with KEYWORD=VALUE settings from the files in
40  'file_list'.
41
42  Explicitly adds the following value from internal calculations:
43
44    OFFICIAL_BUILD
45  """
46  CHROME_BUILD_TYPE = os.environ.get('CHROME_BUILD_TYPE')
47  if CHROME_BUILD_TYPE == '_official' or is_official_build:
48    official_build = '1'
49  else:
50    official_build = '0'
51
52  values = dict(
53    OFFICIAL_BUILD = official_build,
54  )
55
56  for file_name in file_list:
57    FetchValuesFromFile(values, file_name)
58
59  script_dirname = os.path.dirname(os.path.realpath(__file__))
60  if official_build == '1':
61    lastchange_filename = os.path.join(script_dirname, "LASTCHANGE")
62  else:
63    lastchange_filename = os.path.join(script_dirname, "LASTCHANGE.dummy")
64  lastchange_values = {}
65  FetchValuesFromFile(lastchange_values, lastchange_filename)
66
67  for placeholder_key, placeholder_value in values.items():
68    values[placeholder_key] = SubstTemplate(placeholder_value,
69                                            lastchange_values)
70
71  return values
72
73
74def SubstTemplate(contents, values):
75  """
76  Returns the template with substituted values from the specified dictionary.
77
78  Keywords to be substituted are surrounded by '@':  @KEYWORD@.
79
80  No attempt is made to avoid recursive substitution.  The order
81  of evaluation is random based on the order of the keywords returned
82  by the Python dictionary.  So do NOT substitute a value that
83  contains any @KEYWORD@ strings expecting them to be recursively
84  substituted, okay?
85  """
86  for key, val in values.items():
87    try:
88      contents = contents.replace('@' + key + '@', val)
89    except TypeError:
90      print(repr(key), repr(val))
91  return contents
92
93
94def SubstFile(file_name, values):
95  """
96  Returns the contents of the specified file_name with substituted values.
97
98  Substituted values come from the specified dictionary.
99
100  This is like SubstTemplate, except it operates on a file.
101  """
102  with open(file_name, 'r') as f:
103    template = f.read()
104  return SubstTemplate(template, values)
105
106
107def WriteIfChanged(file_name, contents, mode):
108  """
109  Writes the specified contents to the specified file_name.
110
111  Does nothing if the contents aren't different than the current contents.
112  """
113  try:
114    with open(file_name, 'r') as f:
115      old_contents = f.read()
116  except EnvironmentError:
117    pass
118  else:
119    if contents == old_contents and mode == stat.S_IMODE(
120        os.lstat(file_name).st_mode):
121      return
122    os.unlink(file_name)
123  with open(file_name, 'w') as f:
124    f.write(contents)
125  os.chmod(file_name, mode)
126
127
128def BuildParser():
129  """Build argparse parser, with added arguments."""
130  parser = argparse.ArgumentParser()
131  parser.add_argument('-f', '--file', action='append', default=[],
132                      help='Read variables from FILE.')
133  parser.add_argument('-i', '--input', default=None,
134                      help='Read strings to substitute from FILE.')
135  parser.add_argument('-o', '--output', default=None,
136                      help='Write substituted strings to FILE.')
137  parser.add_argument('-t', '--template', default=None,
138                      help='Use TEMPLATE as the strings to substitute.')
139  parser.add_argument('-x',
140                      '--executable',
141                      default=False,
142                      action='store_true',
143                      help='Set the executable bit on the output (on POSIX).')
144  parser.add_argument(
145      '-e',
146      '--eval',
147      action='append',
148      default=[],
149      help='Evaluate VAL after reading variables. Can be used '
150      'to synthesize variables. e.g. -e \'PATCH_HI=int('
151      'PATCH)//256.')
152  parser.add_argument(
153      '-a',
154      '--arch',
155      default=None,
156      choices=android_chrome_version.ARCH_CHOICES,
157      help='Set which cpu architecture the build is for.')
158  parser.add_argument('--os', default=None, help='Set the target os.')
159  parser.add_argument('--official', action='store_true',
160                      help='Whether the current build should be an official '
161                           'build, used in addition to the environment '
162                           'variable.')
163  parser.add_argument('--next',
164                      action='store_true',
165                      help='Whether the current build should be a "next" '
166                      'build, which targets pre-release versions of Android.')
167  parser.add_argument('args', nargs=argparse.REMAINDER,
168                      help='For compatibility: INPUT and OUTPUT can be '
169                           'passed as positional arguments.')
170  return parser
171
172
173def BuildEvals(options, parser):
174  """Construct a dict of passed '-e' arguments for evaluating."""
175  evals = {}
176  for expression in options.eval:
177    try:
178      evals.update(dict([expression.split('=', 1)]))
179    except ValueError:
180      parser.error('-e requires VAR=VAL')
181  return evals
182
183
184def ModifyOptionsCompat(options, parser):
185  """Support compatibility with old versions.
186
187  Specifically, for old versions that considered the first two
188  positional arguments shorthands for --input and --output.
189  """
190  while len(options.args) and (options.input is None or options.output is None):
191    if options.input is None:
192      options.input = options.args.pop(0)
193    elif options.output is None:
194      options.output = options.args.pop(0)
195  if options.args:
196    parser.error('Unexpected arguments: %r' % options.args)
197
198
199def GenerateValues(options, evals):
200  """Construct a dict of raw values used to generate output.
201
202  e.g. this could return a dict like
203  {
204    'BUILD': 74,
205  }
206
207  which would be used to resolve a template like
208  'build = "@BUILD@"' into 'build = "74"'
209
210  """
211  values = FetchValues(options.file, options.official)
212
213  for key, val in evals.items():
214    values[key] = str(eval(val, globals(), values))
215
216  if options.os == 'android':
217    android_chrome_version_codes = android_chrome_version.GenerateVersionCodes(
218        int(values['BUILD']), int(values['PATCH']), options.arch, options.next)
219    values.update(android_chrome_version_codes)
220
221  return values
222
223
224def GenerateOutputContents(options, values):
225  """Construct output string (e.g. from template).
226
227  Arguments:
228  options -- argparse parsed arguments
229  values -- dict with raw values used to resolve the keywords in a template
230    string
231  """
232
233  if options.template is not None:
234    return SubstTemplate(options.template, values)
235  elif options.input:
236    return SubstFile(options.input, values)
237  else:
238    # Generate a default set of version information.
239    return """MAJOR=%(MAJOR)s
240MINOR=%(MINOR)s
241BUILD=%(BUILD)s
242PATCH=%(PATCH)s
243LASTCHANGE=%(LASTCHANGE)s
244OFFICIAL_BUILD=%(OFFICIAL_BUILD)s
245""" % values
246
247
248def GenerateOutputMode(options):
249  """Construct output mode (e.g. from template).
250
251  Arguments:
252  options -- argparse parsed arguments
253  """
254  if options.executable:
255    return 0o755
256  else:
257    return 0o644
258
259
260def BuildOutput(args):
261  """Gets all input and output values needed for writing output."""
262  # Build argparse parser with arguments
263  parser = BuildParser()
264  options = parser.parse_args(args)
265
266  # Get dict of passed '-e' arguments for evaluating
267  evals = BuildEvals(options, parser)
268  # For compatibility with interface that considered first two positional
269  # arguments shorthands for --input and --output.
270  ModifyOptionsCompat(options, parser)
271
272  # Get the raw values that will be used the generate the output
273  values = GenerateValues(options, evals)
274  # Get the output string and mode
275  contents = GenerateOutputContents(options, values)
276  mode = GenerateOutputMode(options)
277
278  return {'options': options, 'contents': contents, 'mode': mode}
279
280
281def main(args):
282  output = BuildOutput(args)
283
284  if output['options'].output is not None:
285    WriteIfChanged(output['options'].output, output['contents'], output['mode'])
286  else:
287    print(output['contents'])
288
289  return 0
290
291
292if __name__ == '__main__':
293  sys.exit(main(sys.argv[1:]))
294