1# Copyright 2013 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import ast 6import contextlib 7import fnmatch 8import json 9import os 10import pipes 11import re 12import shlex 13import shutil 14import stat 15import subprocess 16import sys 17import tempfile 18import zipfile 19 20# Some clients do not add //build/android/gyp to PYTHONPATH. 21import md5_check # pylint: disable=relative-import 22 23sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) 24from pylib.constants import host_paths 25 26COLORAMA_ROOT = os.path.join(host_paths.DIR_SOURCE_ROOT, 27 'third_party', 'colorama', 'src') 28# aapt should ignore OWNERS files in addition the default ignore pattern. 29AAPT_IGNORE_PATTERN = ('!OWNERS:!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:' + 30 '!CVS:!thumbs.db:!picasa.ini:!*~:!*.d.stamp') 31_HERMETIC_TIMESTAMP = (2001, 1, 1, 0, 0, 0) 32_HERMETIC_FILE_ATTR = (0644 << 16L) 33 34 35@contextlib.contextmanager 36def TempDir(): 37 dirname = tempfile.mkdtemp() 38 try: 39 yield dirname 40 finally: 41 shutil.rmtree(dirname) 42 43 44def MakeDirectory(dir_path): 45 try: 46 os.makedirs(dir_path) 47 except OSError: 48 pass 49 50 51def DeleteDirectory(dir_path): 52 if os.path.exists(dir_path): 53 shutil.rmtree(dir_path) 54 55 56def Touch(path, fail_if_missing=False): 57 if fail_if_missing and not os.path.exists(path): 58 raise Exception(path + ' doesn\'t exist.') 59 60 MakeDirectory(os.path.dirname(path)) 61 with open(path, 'a'): 62 os.utime(path, None) 63 64 65def FindInDirectory(directory, filename_filter): 66 files = [] 67 for root, _dirnames, filenames in os.walk(directory): 68 matched_files = fnmatch.filter(filenames, filename_filter) 69 files.extend((os.path.join(root, f) for f in matched_files)) 70 return files 71 72 73def FindInDirectories(directories, filename_filter): 74 all_files = [] 75 for directory in directories: 76 all_files.extend(FindInDirectory(directory, filename_filter)) 77 return all_files 78 79 80def ParseGnList(gn_string): 81 # TODO(brettw) bug 573132: This doesn't handle GN escaping properly, so any 82 # weird characters like $ or \ in the strings will be corrupted. 83 # 84 # The code should import build/gn_helpers.py and then do: 85 # parser = gn_helpers.GNValueParser(gn_string) 86 # return return parser.ParseList() 87 # As of this writing, though, there is a CastShell build script that sends 88 # JSON through this function, and using correct GN parsing corrupts that. 89 # 90 # We need to be consistent about passing either JSON or GN lists through 91 # this function. 92 return ast.literal_eval(gn_string) 93 94 95def ParseGypList(gyp_string): 96 # The ninja generator doesn't support $ in strings, so use ## to 97 # represent $. 98 # TODO(cjhopman): Remove when 99 # https://code.google.com/p/gyp/issues/detail?id=327 100 # is addressed. 101 gyp_string = gyp_string.replace('##', '$') 102 103 if gyp_string.startswith('['): 104 return ParseGnList(gyp_string) 105 return shlex.split(gyp_string) 106 107 108def CheckOptions(options, parser, required=None): 109 if not required: 110 return 111 for option_name in required: 112 if getattr(options, option_name) is None: 113 parser.error('--%s is required' % option_name.replace('_', '-')) 114 115 116def WriteJson(obj, path, only_if_changed=False): 117 old_dump = None 118 if os.path.exists(path): 119 with open(path, 'r') as oldfile: 120 old_dump = oldfile.read() 121 122 new_dump = json.dumps(obj, sort_keys=True, indent=2, separators=(',', ': ')) 123 124 if not only_if_changed or old_dump != new_dump: 125 with open(path, 'w') as outfile: 126 outfile.write(new_dump) 127 128 129def ReadJson(path): 130 with open(path, 'r') as jsonfile: 131 return json.load(jsonfile) 132 133 134class CalledProcessError(Exception): 135 """This exception is raised when the process run by CheckOutput 136 exits with a non-zero exit code.""" 137 138 def __init__(self, cwd, args, output): 139 super(CalledProcessError, self).__init__() 140 self.cwd = cwd 141 self.args = args 142 self.output = output 143 144 def __str__(self): 145 # A user should be able to simply copy and paste the command that failed 146 # into their shell. 147 copyable_command = '( cd {}; {} )'.format(os.path.abspath(self.cwd), 148 ' '.join(map(pipes.quote, self.args))) 149 return 'Command failed: {}\n{}'.format(copyable_command, self.output) 150 151 152# This can be used in most cases like subprocess.check_output(). The output, 153# particularly when the command fails, better highlights the command's failure. 154# If the command fails, raises a build_utils.CalledProcessError. 155def CheckOutput(args, cwd=None, env=None, 156 print_stdout=False, print_stderr=True, 157 stdout_filter=None, 158 stderr_filter=None, 159 fail_func=lambda returncode, stderr: returncode != 0): 160 if not cwd: 161 cwd = os.getcwd() 162 163 child = subprocess.Popen(args, 164 stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env) 165 stdout, stderr = child.communicate() 166 167 if stdout_filter is not None: 168 stdout = stdout_filter(stdout) 169 170 if stderr_filter is not None: 171 stderr = stderr_filter(stderr) 172 173 if fail_func(child.returncode, stderr): 174 raise CalledProcessError(cwd, args, stdout + stderr) 175 176 if print_stdout: 177 sys.stdout.write(stdout) 178 if print_stderr: 179 sys.stderr.write(stderr) 180 181 return stdout 182 183 184def GetModifiedTime(path): 185 # For a symlink, the modified time should be the greater of the link's 186 # modified time and the modified time of the target. 187 return max(os.lstat(path).st_mtime, os.stat(path).st_mtime) 188 189 190def IsTimeStale(output, inputs): 191 if not os.path.exists(output): 192 return True 193 194 output_time = GetModifiedTime(output) 195 for i in inputs: 196 if GetModifiedTime(i) > output_time: 197 return True 198 return False 199 200 201def IsDeviceReady(): 202 device_state = CheckOutput(['adb', 'get-state']) 203 return device_state.strip() == 'device' 204 205 206def CheckZipPath(name): 207 if os.path.normpath(name) != name: 208 raise Exception('Non-canonical zip path: %s' % name) 209 if os.path.isabs(name): 210 raise Exception('Absolute zip path: %s' % name) 211 212 213def IsSymlink(zip_file, name): 214 zi = zip_file.getinfo(name) 215 216 # The two high-order bytes of ZipInfo.external_attr represent 217 # UNIX permissions and file type bits. 218 return stat.S_ISLNK(zi.external_attr >> 16L) 219 220 221def ExtractAll(zip_path, path=None, no_clobber=True, pattern=None, 222 predicate=None): 223 if path is None: 224 path = os.getcwd() 225 elif not os.path.exists(path): 226 MakeDirectory(path) 227 228 if not zipfile.is_zipfile(zip_path): 229 raise Exception('Invalid zip file: %s' % zip_path) 230 231 with zipfile.ZipFile(zip_path) as z: 232 for name in z.namelist(): 233 if name.endswith('/'): 234 continue 235 if pattern is not None: 236 if not fnmatch.fnmatch(name, pattern): 237 continue 238 if predicate and not predicate(name): 239 continue 240 CheckZipPath(name) 241 if no_clobber: 242 output_path = os.path.join(path, name) 243 if os.path.exists(output_path): 244 raise Exception( 245 'Path already exists from zip: %s %s %s' 246 % (zip_path, name, output_path)) 247 if IsSymlink(z, name): 248 dest = os.path.join(path, name) 249 MakeDirectory(os.path.dirname(dest)) 250 os.symlink(z.read(name), dest) 251 else: 252 z.extract(name, path) 253 254 255def AddToZipHermetic(zip_file, zip_path, src_path=None, data=None, 256 compress=None): 257 """Adds a file to the given ZipFile with a hard-coded modified time. 258 259 Args: 260 zip_file: ZipFile instance to add the file to. 261 zip_path: Destination path within the zip file. 262 src_path: Path of the source file. Mutually exclusive with |data|. 263 data: File data as a string. 264 compress: Whether to enable compression. Default is take from ZipFile 265 constructor. 266 """ 267 assert (src_path is None) != (data is None), ( 268 '|src_path| and |data| are mutually exclusive.') 269 CheckZipPath(zip_path) 270 zipinfo = zipfile.ZipInfo(filename=zip_path, date_time=_HERMETIC_TIMESTAMP) 271 zipinfo.external_attr = _HERMETIC_FILE_ATTR 272 273 if src_path and os.path.islink(src_path): 274 zipinfo.filename = zip_path 275 zipinfo.external_attr |= stat.S_IFLNK << 16L # mark as a symlink 276 zip_file.writestr(zipinfo, os.readlink(src_path)) 277 return 278 279 if src_path: 280 with file(src_path) as f: 281 data = f.read() 282 283 # zipfile will deflate even when it makes the file bigger. To avoid 284 # growing files, disable compression at an arbitrary cut off point. 285 if len(data) < 16: 286 compress = False 287 288 # None converts to ZIP_STORED, when passed explicitly rather than the 289 # default passed to the ZipFile constructor. 290 compress_type = zip_file.compression 291 if compress is not None: 292 compress_type = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED 293 zip_file.writestr(zipinfo, data, compress_type) 294 295 296def DoZip(inputs, output, base_dir=None): 297 """Creates a zip file from a list of files. 298 299 Args: 300 inputs: A list of paths to zip, or a list of (zip_path, fs_path) tuples. 301 output: Destination .zip file. 302 base_dir: Prefix to strip from inputs. 303 """ 304 input_tuples = [] 305 for tup in inputs: 306 if isinstance(tup, basestring): 307 tup = (os.path.relpath(tup, base_dir), tup) 308 input_tuples.append(tup) 309 310 # Sort by zip path to ensure stable zip ordering. 311 input_tuples.sort(key=lambda tup: tup[0]) 312 with zipfile.ZipFile(output, 'w') as outfile: 313 for zip_path, fs_path in input_tuples: 314 AddToZipHermetic(outfile, zip_path, src_path=fs_path) 315 316 317def ZipDir(output, base_dir): 318 """Creates a zip file from a directory.""" 319 inputs = [] 320 for root, _, files in os.walk(base_dir): 321 for f in files: 322 inputs.append(os.path.join(root, f)) 323 DoZip(inputs, output, base_dir) 324 325 326def MatchesGlob(path, filters): 327 """Returns whether the given path matches any of the given glob patterns.""" 328 return filters and any(fnmatch.fnmatch(path, f) for f in filters) 329 330 331def MergeZips(output, inputs, exclude_patterns=None, path_transform=None): 332 path_transform = path_transform or (lambda p, z: p) 333 added_names = set() 334 335 with zipfile.ZipFile(output, 'w') as out_zip: 336 for in_file in inputs: 337 with zipfile.ZipFile(in_file, 'r') as in_zip: 338 in_zip._expected_crc = None 339 for info in in_zip.infolist(): 340 # Ignore directories. 341 if info.filename[-1] == '/': 342 continue 343 dst_name = path_transform(info.filename, in_file) 344 already_added = dst_name in added_names 345 if not already_added and not MatchesGlob(dst_name, exclude_patterns): 346 AddToZipHermetic(out_zip, dst_name, data=in_zip.read(info)) 347 added_names.add(dst_name) 348 349 350def PrintWarning(message): 351 print 'WARNING: ' + message 352 353 354def PrintBigWarning(message): 355 print '***** ' * 8 356 PrintWarning(message) 357 print '***** ' * 8 358 359 360def GetSortedTransitiveDependencies(top, deps_func): 361 """Gets the list of all transitive dependencies in sorted order. 362 363 There should be no cycles in the dependency graph. 364 365 Args: 366 top: a list of the top level nodes 367 deps_func: A function that takes a node and returns its direct dependencies. 368 Returns: 369 A list of all transitive dependencies of nodes in top, in order (a node will 370 appear in the list at a higher index than all of its dependencies). 371 """ 372 def Node(dep): 373 return (dep, deps_func(dep)) 374 375 # First: find all deps 376 unchecked_deps = list(top) 377 all_deps = set(top) 378 while unchecked_deps: 379 dep = unchecked_deps.pop() 380 new_deps = deps_func(dep).difference(all_deps) 381 unchecked_deps.extend(new_deps) 382 all_deps = all_deps.union(new_deps) 383 384 # Then: simple, slow topological sort. 385 sorted_deps = [] 386 unsorted_deps = dict(map(Node, all_deps)) 387 while unsorted_deps: 388 for library, dependencies in unsorted_deps.items(): 389 if not dependencies.intersection(unsorted_deps.keys()): 390 sorted_deps.append(library) 391 del unsorted_deps[library] 392 393 return sorted_deps 394 395 396def GetPythonDependencies(): 397 """Gets the paths of imported non-system python modules. 398 399 A path is assumed to be a "system" import if it is outside of chromium's 400 src/. The paths will be relative to the current directory. 401 """ 402 module_paths = (m.__file__ for m in sys.modules.itervalues() 403 if m is not None and hasattr(m, '__file__')) 404 405 abs_module_paths = map(os.path.abspath, module_paths) 406 407 assert os.path.isabs(host_paths.DIR_SOURCE_ROOT) 408 non_system_module_paths = [ 409 p for p in abs_module_paths if p.startswith(host_paths.DIR_SOURCE_ROOT)] 410 def ConvertPycToPy(s): 411 if s.endswith('.pyc'): 412 return s[:-1] 413 return s 414 415 non_system_module_paths = map(ConvertPycToPy, non_system_module_paths) 416 non_system_module_paths = map(os.path.relpath, non_system_module_paths) 417 return sorted(set(non_system_module_paths)) 418 419 420def AddDepfileOption(parser): 421 # TODO(agrieve): Get rid of this once we've moved to argparse. 422 if hasattr(parser, 'add_option'): 423 func = parser.add_option 424 else: 425 func = parser.add_argument 426 func('--depfile', 427 help='Path to depfile. Must be specified as the action\'s first output.') 428 429 430def WriteDepfile(path, dependencies): 431 with open(path, 'w') as depfile: 432 depfile.write(path) 433 depfile.write(': ') 434 depfile.write(' '.join(dependencies)) 435 depfile.write('\n') 436 437 438def ExpandFileArgs(args): 439 """Replaces file-arg placeholders in args. 440 441 These placeholders have the form: 442 @FileArg(filename:key1:key2:...:keyn) 443 444 The value of such a placeholder is calculated by reading 'filename' as json. 445 And then extracting the value at [key1][key2]...[keyn]. 446 447 Note: This intentionally does not return the list of files that appear in such 448 placeholders. An action that uses file-args *must* know the paths of those 449 files prior to the parsing of the arguments (typically by explicitly listing 450 them in the action's inputs in build files). 451 """ 452 new_args = list(args) 453 file_jsons = dict() 454 r = re.compile('@FileArg\((.*?)\)') 455 for i, arg in enumerate(args): 456 match = r.search(arg) 457 if not match: 458 continue 459 460 if match.end() != len(arg): 461 raise Exception('Unexpected characters after FileArg: ' + arg) 462 463 lookup_path = match.group(1).split(':') 464 file_path = lookup_path[0] 465 if not file_path in file_jsons: 466 file_jsons[file_path] = ReadJson(file_path) 467 468 expansion = file_jsons[file_path] 469 for k in lookup_path[1:]: 470 expansion = expansion[k] 471 472 new_args[i] = arg[:match.start()] + str(expansion) 473 474 return new_args 475 476 477def CallAndWriteDepfileIfStale(function, options, record_path=None, 478 input_paths=None, input_strings=None, 479 output_paths=None, force=False, 480 pass_changes=False, 481 depfile_deps=None): 482 """Wraps md5_check.CallAndRecordIfStale() and also writes dep & stamp files. 483 484 Depfiles and stamp files are automatically added to output_paths when present 485 in the |options| argument. They are then created after |function| is called. 486 487 By default, only python dependencies are added to the depfile. If there are 488 other input paths that are not captured by GN deps, then they should be listed 489 in depfile_deps. It's important to write paths to the depfile that are already 490 captured by GN deps since GN args can cause GN deps to change, and such 491 changes are not immediately reflected in depfiles (http://crbug.com/589311). 492 """ 493 if not output_paths: 494 raise Exception('At least one output_path must be specified.') 495 input_paths = list(input_paths or []) 496 input_strings = list(input_strings or []) 497 output_paths = list(output_paths or []) 498 499 python_deps = None 500 if hasattr(options, 'depfile') and options.depfile: 501 python_deps = GetPythonDependencies() 502 input_paths += python_deps 503 output_paths += [options.depfile] 504 505 stamp_file = hasattr(options, 'stamp') and options.stamp 506 if stamp_file: 507 output_paths += [stamp_file] 508 509 def on_stale_md5(changes): 510 args = (changes,) if pass_changes else () 511 function(*args) 512 if python_deps is not None: 513 all_depfile_deps = list(python_deps) 514 if depfile_deps: 515 all_depfile_deps.extend(depfile_deps) 516 WriteDepfile(options.depfile, all_depfile_deps) 517 if stamp_file: 518 Touch(stamp_file) 519 520 md5_check.CallAndRecordIfStale( 521 on_stale_md5, 522 record_path=record_path, 523 input_paths=input_paths, 524 input_strings=input_strings, 525 output_paths=output_paths, 526 force=force, 527 pass_changes=True) 528 529