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