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