version.py (8556B)
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 """ 7 version.py -- Chromium version string substitution utility. 8 """ 9 10 11 import argparse 12 import os 13 import stat 14 import sys 15 16 import android_chrome_version 17 18 19 def 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 35 def 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 74 def 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 94 def 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 107 def 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 128 def 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('args', nargs=argparse.REMAINDER, 164 help='For compatibility: INPUT and OUTPUT can be ' 165 'passed as positional arguments.') 166 return parser 167 168 169 def BuildEvals(options, parser): 170 """Construct a dict of passed '-e' arguments for evaluating.""" 171 evals = {} 172 for expression in options.eval: 173 try: 174 evals.update(dict([expression.split('=', 1)])) 175 except ValueError: 176 parser.error('-e requires VAR=VAL') 177 return evals 178 179 180 def ModifyOptionsCompat(options, parser): 181 """Support compatibility with old versions. 182 183 Specifically, for old versions that considered the first two 184 positional arguments shorthands for --input and --output. 185 """ 186 while len(options.args) and (options.input is None or options.output is None): 187 if options.input is None: 188 options.input = options.args.pop(0) 189 elif options.output is None: 190 options.output = options.args.pop(0) 191 if options.args: 192 parser.error('Unexpected arguments: %r' % options.args) 193 194 195 def GenerateValues(options, evals): 196 """Construct a dict of raw values used to generate output. 197 198 e.g. this could return a dict like 199 { 200 'BUILD': 74, 201 } 202 203 which would be used to resolve a template like 204 'build = "@BUILD@"' into 'build = "74"' 205 206 """ 207 values = FetchValues(options.file, options.official) 208 209 for key, val in evals.items(): 210 values[key] = str(eval(val, globals(), values)) 211 212 if options.os == 'android': 213 android_chrome_version_codes = android_chrome_version.GenerateVersionCodes( 214 int(values['BUILD']), int(values['PATCH']), options.arch) 215 values.update(android_chrome_version_codes) 216 217 return values 218 219 220 def GenerateOutputContents(options, values): 221 """Construct output string (e.g. from template). 222 223 Arguments: 224 options -- argparse parsed arguments 225 values -- dict with raw values used to resolve the keywords in a template 226 string 227 """ 228 229 if options.template is not None: 230 return SubstTemplate(options.template, values) 231 elif options.input: 232 return SubstFile(options.input, values) 233 else: 234 # Generate a default set of version information. 235 return """MAJOR=%(MAJOR)s 236 MINOR=%(MINOR)s 237 BUILD=%(BUILD)s 238 PATCH=%(PATCH)s 239 LASTCHANGE=%(LASTCHANGE)s 240 OFFICIAL_BUILD=%(OFFICIAL_BUILD)s 241 """ % values 242 243 244 def GenerateOutputMode(options): 245 """Construct output mode (e.g. from template). 246 247 Arguments: 248 options -- argparse parsed arguments 249 """ 250 if options.executable: 251 return 0o755 252 else: 253 return 0o644 254 255 256 def BuildOutput(args): 257 """Gets all input and output values needed for writing output.""" 258 # Build argparse parser with arguments 259 parser = BuildParser() 260 options = parser.parse_args(args) 261 262 # Get dict of passed '-e' arguments for evaluating 263 evals = BuildEvals(options, parser) 264 # For compatibility with interface that considered first two positional 265 # arguments shorthands for --input and --output. 266 ModifyOptionsCompat(options, parser) 267 268 # Get the raw values that will be used the generate the output 269 values = GenerateValues(options, evals) 270 # Get the output string and mode 271 contents = GenerateOutputContents(options, values) 272 mode = GenerateOutputMode(options) 273 274 return {'options': options, 'contents': contents, 'mode': mode} 275 276 277 def main(args): 278 output = BuildOutput(args) 279 280 if output['options'].output is not None: 281 WriteIfChanged(output['options'].output, output['contents'], output['mode']) 282 else: 283 print(output['contents']) 284 285 return 0 286 287 288 if __name__ == '__main__': 289 sys.exit(main(sys.argv[1:]))