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 subprocess 15import sys 16import tempfile 17import zipfile 18 19 20CHROMIUM_SRC = os.path.normpath( 21 os.path.join(os.path.dirname(__file__), 22 os.pardir, os.pardir, os.pardir, os.pardir)) 23COLORAMA_ROOT = os.path.join(CHROMIUM_SRC, 24 'third_party', 'colorama', 'src') 25 26 27@contextlib.contextmanager 28def TempDir(): 29 dirname = tempfile.mkdtemp() 30 try: 31 yield dirname 32 finally: 33 shutil.rmtree(dirname) 34 35 36def MakeDirectory(dir_path): 37 try: 38 os.makedirs(dir_path) 39 except OSError: 40 pass 41 42 43def DeleteDirectory(dir_path): 44 if os.path.exists(dir_path): 45 shutil.rmtree(dir_path) 46 47 48def Touch(path, fail_if_missing=False): 49 if fail_if_missing and not os.path.exists(path): 50 raise Exception(path + ' doesn\'t exist.') 51 52 MakeDirectory(os.path.dirname(path)) 53 with open(path, 'a'): 54 os.utime(path, None) 55 56 57def FindInDirectory(directory, filename_filter): 58 files = [] 59 for root, _dirnames, filenames in os.walk(directory): 60 matched_files = fnmatch.filter(filenames, filename_filter) 61 files.extend((os.path.join(root, f) for f in matched_files)) 62 return files 63 64 65def FindInDirectories(directories, filename_filter): 66 all_files = [] 67 for directory in directories: 68 all_files.extend(FindInDirectory(directory, filename_filter)) 69 return all_files 70 71 72def ParseGnList(gn_string): 73 return ast.literal_eval(gn_string) 74 75 76def ParseGypList(gyp_string): 77 # The ninja generator doesn't support $ in strings, so use ## to 78 # represent $. 79 # TODO(cjhopman): Remove when 80 # https://code.google.com/p/gyp/issues/detail?id=327 81 # is addressed. 82 gyp_string = gyp_string.replace('##', '$') 83 84 if gyp_string.startswith('['): 85 return ParseGnList(gyp_string) 86 return shlex.split(gyp_string) 87 88 89def CheckOptions(options, parser, required=None): 90 if not required: 91 return 92 for option_name in required: 93 if getattr(options, option_name) is None: 94 parser.error('--%s is required' % option_name.replace('_', '-')) 95 96 97def WriteJson(obj, path, only_if_changed=False): 98 old_dump = None 99 if os.path.exists(path): 100 with open(path, 'r') as oldfile: 101 old_dump = oldfile.read() 102 103 new_dump = json.dumps(obj, sort_keys=True, indent=2, separators=(',', ': ')) 104 105 if not only_if_changed or old_dump != new_dump: 106 with open(path, 'w') as outfile: 107 outfile.write(new_dump) 108 109 110def ReadJson(path): 111 with open(path, 'r') as jsonfile: 112 return json.load(jsonfile) 113 114 115class CalledProcessError(Exception): 116 """This exception is raised when the process run by CheckOutput 117 exits with a non-zero exit code.""" 118 119 def __init__(self, cwd, args, output): 120 super(CalledProcessError, self).__init__() 121 self.cwd = cwd 122 self.args = args 123 self.output = output 124 125 def __str__(self): 126 # A user should be able to simply copy and paste the command that failed 127 # into their shell. 128 copyable_command = '( cd {}; {} )'.format(os.path.abspath(self.cwd), 129 ' '.join(map(pipes.quote, self.args))) 130 return 'Command failed: {}\n{}'.format(copyable_command, self.output) 131 132 133# This can be used in most cases like subprocess.check_output(). The output, 134# particularly when the command fails, better highlights the command's failure. 135# If the command fails, raises a build_utils.CalledProcessError. 136def CheckOutput(args, cwd=None, 137 print_stdout=False, print_stderr=True, 138 stdout_filter=None, 139 stderr_filter=None, 140 fail_func=lambda returncode, stderr: returncode != 0): 141 if not cwd: 142 cwd = os.getcwd() 143 144 child = subprocess.Popen(args, 145 stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) 146 stdout, stderr = child.communicate() 147 148 if stdout_filter is not None: 149 stdout = stdout_filter(stdout) 150 151 if stderr_filter is not None: 152 stderr = stderr_filter(stderr) 153 154 if fail_func(child.returncode, stderr): 155 raise CalledProcessError(cwd, args, stdout + stderr) 156 157 if print_stdout: 158 sys.stdout.write(stdout) 159 if print_stderr: 160 sys.stderr.write(stderr) 161 162 return stdout 163 164 165def GetModifiedTime(path): 166 # For a symlink, the modified time should be the greater of the link's 167 # modified time and the modified time of the target. 168 return max(os.lstat(path).st_mtime, os.stat(path).st_mtime) 169 170 171def IsTimeStale(output, inputs): 172 if not os.path.exists(output): 173 return True 174 175 output_time = GetModifiedTime(output) 176 for i in inputs: 177 if GetModifiedTime(i) > output_time: 178 return True 179 return False 180 181 182def IsDeviceReady(): 183 device_state = CheckOutput(['adb', 'get-state']) 184 return device_state.strip() == 'device' 185 186 187def CheckZipPath(name): 188 if os.path.normpath(name) != name: 189 raise Exception('Non-canonical zip path: %s' % name) 190 if os.path.isabs(name): 191 raise Exception('Absolute zip path: %s' % name) 192 193 194def ExtractAll(zip_path, path=None, no_clobber=True, pattern=None): 195 if path is None: 196 path = os.getcwd() 197 elif not os.path.exists(path): 198 MakeDirectory(path) 199 200 with zipfile.ZipFile(zip_path) as z: 201 for name in z.namelist(): 202 if name.endswith('/'): 203 continue 204 if pattern is not None: 205 if not fnmatch.fnmatch(name, pattern): 206 continue 207 CheckZipPath(name) 208 if no_clobber: 209 output_path = os.path.join(path, name) 210 if os.path.exists(output_path): 211 raise Exception( 212 'Path already exists from zip: %s %s %s' 213 % (zip_path, name, output_path)) 214 215 z.extractall(path=path) 216 217 218def DoZip(inputs, output, base_dir): 219 with zipfile.ZipFile(output, 'w') as outfile: 220 for f in inputs: 221 CheckZipPath(os.path.relpath(f, base_dir)) 222 outfile.write(f, os.path.relpath(f, base_dir)) 223 224 225def ZipDir(output, base_dir): 226 with zipfile.ZipFile(output, 'w') as outfile: 227 for root, _, files in os.walk(base_dir): 228 for f in files: 229 path = os.path.join(root, f) 230 archive_path = os.path.relpath(path, base_dir) 231 CheckZipPath(archive_path) 232 outfile.write(path, archive_path) 233 234 235def MergeZips(output, inputs, exclude_patterns=None): 236 def Allow(name): 237 if exclude_patterns is not None: 238 for p in exclude_patterns: 239 if fnmatch.fnmatch(name, p): 240 return False 241 return True 242 243 with zipfile.ZipFile(output, 'w') as out_zip: 244 for in_file in inputs: 245 with zipfile.ZipFile(in_file, 'r') as in_zip: 246 for name in in_zip.namelist(): 247 if Allow(name): 248 out_zip.writestr(name, in_zip.read(name)) 249 250 251def PrintWarning(message): 252 print 'WARNING: ' + message 253 254 255def PrintBigWarning(message): 256 print '***** ' * 8 257 PrintWarning(message) 258 print '***** ' * 8 259 260 261def GetSortedTransitiveDependencies(top, deps_func): 262 """Gets the list of all transitive dependencies in sorted order. 263 264 There should be no cycles in the dependency graph. 265 266 Args: 267 top: a list of the top level nodes 268 deps_func: A function that takes a node and returns its direct dependencies. 269 Returns: 270 A list of all transitive dependencies of nodes in top, in order (a node will 271 appear in the list at a higher index than all of its dependencies). 272 """ 273 def Node(dep): 274 return (dep, deps_func(dep)) 275 276 # First: find all deps 277 unchecked_deps = list(top) 278 all_deps = set(top) 279 while unchecked_deps: 280 dep = unchecked_deps.pop() 281 new_deps = deps_func(dep).difference(all_deps) 282 unchecked_deps.extend(new_deps) 283 all_deps = all_deps.union(new_deps) 284 285 # Then: simple, slow topological sort. 286 sorted_deps = [] 287 unsorted_deps = dict(map(Node, all_deps)) 288 while unsorted_deps: 289 for library, dependencies in unsorted_deps.items(): 290 if not dependencies.intersection(unsorted_deps.keys()): 291 sorted_deps.append(library) 292 del unsorted_deps[library] 293 294 return sorted_deps 295 296 297def GetPythonDependencies(): 298 """Gets the paths of imported non-system python modules. 299 300 A path is assumed to be a "system" import if it is outside of chromium's 301 src/. The paths will be relative to the current directory. 302 """ 303 module_paths = (m.__file__ for m in sys.modules.itervalues() 304 if m is not None and hasattr(m, '__file__')) 305 306 abs_module_paths = map(os.path.abspath, module_paths) 307 308 non_system_module_paths = [ 309 p for p in abs_module_paths if p.startswith(CHROMIUM_SRC)] 310 def ConvertPycToPy(s): 311 if s.endswith('.pyc'): 312 return s[:-1] 313 return s 314 315 non_system_module_paths = map(ConvertPycToPy, non_system_module_paths) 316 non_system_module_paths = map(os.path.relpath, non_system_module_paths) 317 return sorted(set(non_system_module_paths)) 318 319 320def AddDepfileOption(parser): 321 parser.add_option('--depfile', 322 help='Path to depfile. This must be specified as the ' 323 'action\'s first output.') 324 325 326def WriteDepfile(path, dependencies): 327 with open(path, 'w') as depfile: 328 depfile.write(path) 329 depfile.write(': ') 330 depfile.write(' '.join(dependencies)) 331 depfile.write('\n') 332 333 334def ExpandFileArgs(args): 335 """Replaces file-arg placeholders in args. 336 337 These placeholders have the form: 338 @FileArg(filename:key1:key2:...:keyn) 339 340 The value of such a placeholder is calculated by reading 'filename' as json. 341 And then extracting the value at [key1][key2]...[keyn]. 342 343 Note: This intentionally does not return the list of files that appear in such 344 placeholders. An action that uses file-args *must* know the paths of those 345 files prior to the parsing of the arguments (typically by explicitly listing 346 them in the action's inputs in build files). 347 """ 348 new_args = list(args) 349 file_jsons = dict() 350 r = re.compile('@FileArg\((.*?)\)') 351 for i, arg in enumerate(args): 352 match = r.search(arg) 353 if not match: 354 continue 355 356 if match.end() != len(arg): 357 raise Exception('Unexpected characters after FileArg: ' + arg) 358 359 lookup_path = match.group(1).split(':') 360 file_path = lookup_path[0] 361 if not file_path in file_jsons: 362 file_jsons[file_path] = ReadJson(file_path) 363 364 expansion = file_jsons[file_path] 365 for k in lookup_path[1:]: 366 expansion = expansion[k] 367 368 new_args[i] = arg[:match.start()] + str(expansion) 369 370 return new_args 371 372