xref: /aosp_15_r20/external/cronet/build/util/version.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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  lastchange_filename = os.path.join(script_dirname, "LASTCHANGE")
61  lastchange_values = {}
62  FetchValuesFromFile(lastchange_values, lastchange_filename)
63
64  for placeholder_key, placeholder_value in values.items():
65    values[placeholder_key] = SubstTemplate(placeholder_value,
66                                            lastchange_values)
67
68  return values
69
70
71def SubstTemplate(contents, values):
72  """
73  Returns the template with substituted values from the specified dictionary.
74
75  Keywords to be substituted are surrounded by '@':  @KEYWORD@.
76
77  No attempt is made to avoid recursive substitution.  The order
78  of evaluation is random based on the order of the keywords returned
79  by the Python dictionary.  So do NOT substitute a value that
80  contains any @KEYWORD@ strings expecting them to be recursively
81  substituted, okay?
82  """
83  for key, val in values.items():
84    try:
85      contents = contents.replace('@' + key + '@', val)
86    except TypeError:
87      print(repr(key), repr(val))
88  return contents
89
90
91def SubstFile(file_name, values):
92  """
93  Returns the contents of the specified file_name with substituted values.
94
95  Substituted values come from the specified dictionary.
96
97  This is like SubstTemplate, except it operates on a file.
98  """
99  with open(file_name, 'r') as f:
100    template = f.read()
101  return SubstTemplate(template, values)
102
103
104def WriteIfChanged(file_name, contents, mode):
105  """
106  Writes the specified contents to the specified file_name.
107
108  Does nothing if the contents aren't different than the current contents.
109  """
110  try:
111    with open(file_name, 'r') as f:
112      old_contents = f.read()
113  except EnvironmentError:
114    pass
115  else:
116    if contents == old_contents and mode == stat.S_IMODE(
117        os.lstat(file_name).st_mode):
118      return
119    os.unlink(file_name)
120  with open(file_name, 'w') as f:
121    f.write(contents)
122  os.chmod(file_name, mode)
123
124
125def BuildParser():
126  """Build argparse parser, with added arguments."""
127  parser = argparse.ArgumentParser()
128  parser.add_argument('-f', '--file', action='append', default=[],
129                      help='Read variables from FILE.')
130  parser.add_argument('-i', '--input', default=None,
131                      help='Read strings to substitute from FILE.')
132  parser.add_argument('-o', '--output', default=None,
133                      help='Write substituted strings to FILE.')
134  parser.add_argument('-t', '--template', default=None,
135                      help='Use TEMPLATE as the strings to substitute.')
136  parser.add_argument('-x',
137                      '--executable',
138                      default=False,
139                      action='store_true',
140                      help='Set the executable bit on the output (on POSIX).')
141  parser.add_argument(
142      '-e',
143      '--eval',
144      action='append',
145      default=[],
146      help='Evaluate VAL after reading variables. Can be used '
147      'to synthesize variables. e.g. -e \'PATCH_HI=int('
148      'PATCH)//256.')
149  parser.add_argument(
150      '-a',
151      '--arch',
152      default=None,
153      choices=android_chrome_version.ARCH_CHOICES,
154      help='Set which cpu architecture the build is for.')
155  parser.add_argument('--os', default=None, help='Set the target os.')
156  parser.add_argument('--official', action='store_true',
157                      help='Whether the current build should be an official '
158                           'build, used in addition to the environment '
159                           'variable.')
160  parser.add_argument('--next',
161                      action='store_true',
162                      help='Whether the current build should be a "next" '
163                      'build, which targets pre-release versions of Android.')
164  parser.add_argument('args', nargs=argparse.REMAINDER,
165                      help='For compatibility: INPUT and OUTPUT can be '
166                           'passed as positional arguments.')
167  return parser
168
169
170def BuildEvals(options, parser):
171  """Construct a dict of passed '-e' arguments for evaluating."""
172  evals = {}
173  for expression in options.eval:
174    try:
175      evals.update(dict([expression.split('=', 1)]))
176    except ValueError:
177      parser.error('-e requires VAR=VAL')
178  return evals
179
180
181def ModifyOptionsCompat(options, parser):
182  """Support compatibility with old versions.
183
184  Specifically, for old versions that considered the first two
185  positional arguments shorthands for --input and --output.
186  """
187  while len(options.args) and (options.input is None or options.output is None):
188    if options.input is None:
189      options.input = options.args.pop(0)
190    elif options.output is None:
191      options.output = options.args.pop(0)
192  if options.args:
193    parser.error('Unexpected arguments: %r' % options.args)
194
195
196def GenerateValues(options, evals):
197  """Construct a dict of raw values used to generate output.
198
199  e.g. this could return a dict like
200  {
201    'BUILD': 74,
202  }
203
204  which would be used to resolve a template like
205  'build = "@BUILD@"' into 'build = "74"'
206
207  """
208  values = FetchValues(options.file, options.official)
209
210  for key, val in evals.items():
211    values[key] = str(eval(val, globals(), values))
212
213  if options.os == 'android':
214    android_chrome_version_codes = android_chrome_version.GenerateVersionCodes(
215        int(values['BUILD']), int(values['PATCH']), options.arch, options.next)
216    values.update(android_chrome_version_codes)
217
218  return values
219
220
221def GenerateOutputContents(options, values):
222  """Construct output string (e.g. from template).
223
224  Arguments:
225  options -- argparse parsed arguments
226  values -- dict with raw values used to resolve the keywords in a template
227    string
228  """
229
230  if options.template is not None:
231    return SubstTemplate(options.template, values)
232  elif options.input:
233    return SubstFile(options.input, values)
234  else:
235    # Generate a default set of version information.
236    return """MAJOR=%(MAJOR)s
237MINOR=%(MINOR)s
238BUILD=%(BUILD)s
239PATCH=%(PATCH)s
240LASTCHANGE=%(LASTCHANGE)s
241OFFICIAL_BUILD=%(OFFICIAL_BUILD)s
242""" % values
243
244
245def GenerateOutputMode(options):
246  """Construct output mode (e.g. from template).
247
248  Arguments:
249  options -- argparse parsed arguments
250  """
251  if options.executable:
252    return 0o755
253  else:
254    return 0o644
255
256
257def BuildOutput(args):
258  """Gets all input and output values needed for writing output."""
259  # Build argparse parser with arguments
260  parser = BuildParser()
261  options = parser.parse_args(args)
262
263  # Get dict of passed '-e' arguments for evaluating
264  evals = BuildEvals(options, parser)
265  # For compatibility with interface that considered first two positional
266  # arguments shorthands for --input and --output.
267  ModifyOptionsCompat(options, parser)
268
269  # Get the raw values that will be used the generate the output
270  values = GenerateValues(options, evals)
271  # Get the output string and mode
272  contents = GenerateOutputContents(options, values)
273  mode = GenerateOutputMode(options)
274
275  return {'options': options, 'contents': contents, 'mode': mode}
276
277
278def main(args):
279  output = BuildOutput(args)
280
281  if output['options'].output is not None:
282    WriteIfChanged(output['options'].output, output['contents'], output['mode'])
283  else:
284    print(output['contents'])
285
286  return 0
287
288
289if __name__ == '__main__':
290  sys.exit(main(sys.argv[1:]))
291