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