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