OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2012 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 |
| 5 """ Set of basic operations/utilities that are used by the build. """ |
| 6 |
| 7 from contextlib import contextmanager |
| 8 import ast |
| 9 import base64 |
| 10 import cStringIO |
| 11 import copy |
| 12 import errno |
| 13 import fnmatch |
| 14 import glob |
| 15 import math |
| 16 import multiprocessing |
| 17 import os |
| 18 import re |
| 19 import shutil |
| 20 import socket |
| 21 import stat |
| 22 import string # pylint: disable=W0402 |
| 23 import subprocess |
| 24 import sys |
| 25 import threading |
| 26 import time |
| 27 import traceback |
| 28 import urllib |
| 29 import zipfile |
| 30 import zlib |
| 31 |
| 32 try: |
| 33 import json # pylint: disable=F0401 |
| 34 except ImportError: |
| 35 import simplejson as json |
| 36 |
| 37 from common import env |
| 38 |
| 39 |
| 40 BUILD_DIR = os.path.realpath(os.path.join( |
| 41 os.path.dirname(__file__), os.pardir, os.pardir)) |
| 42 |
| 43 |
| 44 WIN_LINK_FUNC = None |
| 45 try: |
| 46 if sys.platform.startswith('win'): |
| 47 import ctypes |
| 48 # There's 4 possibilities on Windows for links: |
| 49 # 1. Symbolic file links; |
| 50 # 2. Symbolic directory links; |
| 51 # 3. Hardlinked files; |
| 52 # 4. Junctioned directories. |
| 53 # (Hardlinked directories don't really exist.) |
| 54 # |
| 55 # 7-Zip does not handle symbolic file links as we want (it puts the |
| 56 # content of the link, not what it refers to, and reports "CRC Error" on |
| 57 # extraction). It does work as expected for symbolic directory links. |
| 58 # Because the majority of the large files are in the root of the staging |
| 59 # directory, we do however need to handle file links, so we do this with |
| 60 # hardlinking. Junctioning requires a huge whack of code, so we take the |
| 61 # slightly odd tactic of using #2 and #3, but not #1 and #4. That is, |
| 62 # hardlinks for files, but symbolic links for directories. |
| 63 def _WIN_LINK_FUNC(src, dst): |
| 64 print 'linking %s -> %s' % (src, dst) |
| 65 if os.path.isdir(src): |
| 66 if not ctypes.windll.kernel32.CreateSymbolicLinkA( |
| 67 str(dst), str(os.path.abspath(src)), 1): |
| 68 raise ctypes.WinError() |
| 69 else: |
| 70 if not ctypes.windll.kernel32.CreateHardLinkA(str(dst), str(src), 0): |
| 71 raise ctypes.WinError() |
| 72 WIN_LINK_FUNC = _WIN_LINK_FUNC |
| 73 except ImportError: |
| 74 # If we don't have ctypes or aren't on Windows, leave WIN_LINK_FUNC as None. |
| 75 pass |
| 76 |
| 77 |
| 78 # Wrapper around git that enforces a timeout. |
| 79 GIT_BIN = os.path.join(BUILD_DIR, 'scripts', 'tools', 'git-with-timeout') |
| 80 |
| 81 # Wrapper around svn that enforces a timeout. |
| 82 SVN_BIN = os.path.join(BUILD_DIR, 'scripts', 'tools', 'svn-with-timeout') |
| 83 |
| 84 # The Google Storage metadata key for the full commit position |
| 85 GS_COMMIT_POSITION_KEY = 'Cr-Commit-Position' |
| 86 # The Google Storage metadata key for the commit position number |
| 87 GS_COMMIT_POSITION_NUMBER_KEY = 'Cr-Commit-Position-Number' |
| 88 # The Google Storage metadata key for the Git commit hash |
| 89 GS_GIT_COMMIT_KEY = 'Cr-Git-Commit' |
| 90 |
| 91 # Regular expression to identify a Git hash |
| 92 GIT_COMMIT_HASH_RE = re.compile(r'[a-zA-Z0-9]{40}') |
| 93 # |
| 94 # Regular expression to parse a commit position |
| 95 COMMIT_POSITION_RE = re.compile(r'([^@]+)@{#(\d+)}') |
| 96 |
| 97 # Local errors. |
| 98 class MissingArgument(Exception): |
| 99 pass |
| 100 class PathNotFound(Exception): |
| 101 pass |
| 102 class ExternalError(Exception): |
| 103 pass |
| 104 class NoIdentifiedRevision(Exception): |
| 105 pass |
| 106 |
| 107 def IsWindows(): |
| 108 return sys.platform == 'cygwin' or sys.platform.startswith('win') |
| 109 |
| 110 def IsLinux(): |
| 111 return sys.platform.startswith('linux') |
| 112 |
| 113 def IsMac(): |
| 114 return sys.platform.startswith('darwin') |
| 115 |
| 116 # For chromeos we need to end up with a different platform name, but the |
| 117 # scripts use the values like sys.platform for both the build target and |
| 118 # and the running OS, so this gives us a back door that can be hit to |
| 119 # force different naming then the default for some of the chromeos build |
| 120 # steps. |
| 121 override_platform_name = None |
| 122 |
| 123 |
| 124 def OverridePlatformName(name): |
| 125 """Sets the override for PlatformName()""" |
| 126 global override_platform_name |
| 127 override_platform_name = name |
| 128 |
| 129 |
| 130 def PlatformName(): |
| 131 """Return a string to be used in paths for the platform.""" |
| 132 if override_platform_name: |
| 133 return override_platform_name |
| 134 if IsWindows(): |
| 135 return 'win32' |
| 136 if IsLinux(): |
| 137 return 'linux' |
| 138 if IsMac(): |
| 139 return 'mac' |
| 140 raise NotImplementedError('Unknown platform "%s".' % sys.platform) |
| 141 |
| 142 |
| 143 # Name of the file (inside the packaged build) containing revision number |
| 144 # of that build. Also used for determining the latest packaged build. |
| 145 FULL_BUILD_REVISION_FILENAME = 'FULL_BUILD_REVISION' |
| 146 |
| 147 def IsGitCommit(value): |
| 148 """Returns: If a value is a Git commit hash. |
| 149 |
| 150 This only works on full Git commit hashes. A value qualifies as a Git commit |
| 151 hash if it only contains hexadecimal numbers and is forty characters long. |
| 152 """ |
| 153 if value is None: |
| 154 return False |
| 155 return GIT_COMMIT_HASH_RE.match(str(value)) is not None |
| 156 |
| 157 |
| 158 # GetParentClass allows a class instance to find its parent class using Python's |
| 159 # inspect module. This allows a class instantiated from a module to access |
| 160 # their parent class's methods even after the containing module has been |
| 161 # re-imported and reloaded. |
| 162 # |
| 163 # Also see: |
| 164 # http://code.google.com/p/chromium/issues/detail?id=34089 |
| 165 # http://atlee.ca/blog/2008/11/21/python-reload-danger-here-be-dragons/ |
| 166 # |
| 167 def GetParentClass(obj, n=1): |
| 168 import inspect |
| 169 if inspect.isclass(obj): |
| 170 return inspect.getmro(obj)[n] |
| 171 else: |
| 172 return inspect.getmro(obj.__class__)[n] |
| 173 |
| 174 |
| 175 def MeanAndStandardDeviation(data): |
| 176 """Calculates mean and standard deviation for the values in the list. |
| 177 |
| 178 Args: |
| 179 data: list of numbers |
| 180 |
| 181 Returns: |
| 182 Mean and standard deviation for the numbers in the list. |
| 183 """ |
| 184 n = len(data) |
| 185 if n == 0: |
| 186 return 0.0, 0.0 |
| 187 mean = float(sum(data)) / n |
| 188 variance = sum([(element - mean)**2 for element in data]) / n |
| 189 return mean, math.sqrt(variance) |
| 190 |
| 191 |
| 192 def FilteredMeanAndStandardDeviation(data): |
| 193 """Calculates mean and standard deviation for the values in the list |
| 194 ignoring first occurence of max value (unless there was only one sample). |
| 195 |
| 196 Args: |
| 197 data: list of numbers |
| 198 |
| 199 Returns: |
| 200 Mean and standard deviation for the numbers in the list ignoring |
| 201 first occurence of max value. |
| 202 """ |
| 203 |
| 204 def _FilterMax(array): |
| 205 new_array = copy.copy(array) # making sure we are not creating side-effects |
| 206 if len(new_array) != 1: |
| 207 new_array.remove(max(new_array)) |
| 208 return new_array |
| 209 return MeanAndStandardDeviation(_FilterMax(data)) |
| 210 |
| 211 def HistogramPercentiles(histogram, percentiles): |
| 212 if not 'buckets' in histogram or not 'count' in histogram: |
| 213 return [] |
| 214 computed_percentiles = _ComputePercentiles(histogram['buckets'], |
| 215 histogram['count'], |
| 216 percentiles) |
| 217 output = [] |
| 218 for p in computed_percentiles: |
| 219 output.append({'percentile': p, 'value': computed_percentiles[p]}) |
| 220 return output |
| 221 |
| 222 def GeomMeanAndStdDevFromHistogram(histogram): |
| 223 if not 'buckets' in histogram: |
| 224 return 0.0, 0.0 |
| 225 count = 0 |
| 226 sum_of_logs = 0 |
| 227 for bucket in histogram['buckets']: |
| 228 if 'high' in bucket: |
| 229 bucket['mean'] = (bucket['low'] + bucket['high']) / 2.0 |
| 230 else: |
| 231 bucket['mean'] = bucket['low'] |
| 232 if bucket['mean'] > 0: |
| 233 sum_of_logs += math.log(bucket['mean']) * bucket['count'] |
| 234 count += bucket['count'] |
| 235 |
| 236 if count == 0: |
| 237 return 0.0, 0.0 |
| 238 |
| 239 sum_of_squares = 0 |
| 240 geom_mean = math.exp(sum_of_logs / count) |
| 241 for bucket in histogram['buckets']: |
| 242 if bucket['mean'] > 0: |
| 243 sum_of_squares += (bucket['mean'] - geom_mean) ** 2 * bucket['count'] |
| 244 return geom_mean, math.sqrt(sum_of_squares / count) |
| 245 |
| 246 def _LinearInterpolate(x0, target, x1, y0, y1): |
| 247 """Perform linear interpolation to estimate an intermediate value. |
| 248 |
| 249 We assume for some F, F(x0) == y0, and F(x1) == z1. |
| 250 |
| 251 We return an estimate for what F(target) should be, using linear |
| 252 interpolation. |
| 253 |
| 254 Args: |
| 255 x0: (Float) A location at which some function F() is known. |
| 256 target: (Float) A location at which we need to estimate F(). |
| 257 x1: (Float) A second location at which F() is known. |
| 258 y0: (Float) The value of F(x0). |
| 259 y1: (Float) The value of F(x1). |
| 260 |
| 261 Returns: |
| 262 (Float) The estimated value of F(target). |
| 263 """ |
| 264 if x0 == x1: |
| 265 return (y0 + y1) / 2 |
| 266 return (y1 - y0) * (target - x0) / (x1 - x0) + y0 |
| 267 |
| 268 def _BucketInterpolate(last_percentage, target, next_percentage, bucket_min, |
| 269 bucket_max): |
| 270 """Estimate a minimum which should have the target % of samples below it. |
| 271 |
| 272 We do linear interpolation only if last_percentage and next_percentage are |
| 273 adjacent, and hence we are in a linear section of a histogram. Once they |
| 274 spread further apart we generally get exponentially broader buckets, and we |
| 275 need to interpolate in the log domain (and exponentiate our result). |
| 276 |
| 277 Args: |
| 278 last_percentage: (Float) This is the percentage of samples below bucket_min. |
| 279 target: (Float) A percentage for which we need an estimated bucket. |
| 280 next_percentage: (Float) This is the percentage of samples below bucket_max. |
| 281 bucket_min: (Float) This is the lower value for samples in a bucket. |
| 282 bucket_max: (Float) This exceeds the upper value for samples. |
| 283 |
| 284 Returns: |
| 285 (Float) An estimate of what bucket cutoff would have probably had the target |
| 286 percentage. |
| 287 """ |
| 288 log_domain = False |
| 289 if bucket_min + 1.5 < bucket_max and bucket_min > 0: |
| 290 log_domain = True |
| 291 bucket_min = math.log(bucket_min) |
| 292 bucket_max = math.log(bucket_max) |
| 293 result = _LinearInterpolate( |
| 294 last_percentage, target, next_percentage, bucket_min, bucket_max) |
| 295 if log_domain: |
| 296 result = math.exp(result) |
| 297 return result |
| 298 |
| 299 def _ComputePercentiles(buckets, total, percentiles): |
| 300 """Compute percentiles for the given histogram. |
| 301 |
| 302 Returns estimates for the bucket cutoffs that would probably have the taret |
| 303 percentiles. |
| 304 |
| 305 Args: |
| 306 buckets: (List) A list of buckets representing the histogram to analyze. |
| 307 total: (Float) The total number of samples in the histogram. |
| 308 percentiles: (Tuple) The percentiles we are interested in. |
| 309 |
| 310 Returns: |
| 311 (Dictionary) Map from percentiles to bucket cutoffs. |
| 312 """ |
| 313 if not percentiles: |
| 314 return {} |
| 315 current_count = 0 |
| 316 current_percentage = 0 |
| 317 next_percentile_index = 0 |
| 318 result = {} |
| 319 for bucket in buckets: |
| 320 if bucket['count'] > 0: |
| 321 current_count += bucket['count'] |
| 322 old_percentage = current_percentage |
| 323 current_percentage = float(current_count) / total |
| 324 |
| 325 # Check whether we passed one of the percentiles we're interested in. |
| 326 while (next_percentile_index < len(percentiles) and |
| 327 current_percentage > percentiles[next_percentile_index]): |
| 328 if not 'high' in bucket: |
| 329 result[percentiles[next_percentile_index]] = bucket['low'] |
| 330 else: |
| 331 result[percentiles[next_percentile_index]] = float(_BucketInterpolate( |
| 332 old_percentage, percentiles[next_percentile_index], |
| 333 current_percentage, bucket['low'], bucket['high'])) |
| 334 next_percentile_index += 1 |
| 335 return result |
| 336 |
| 337 class InitializePartiallyWithArguments: |
| 338 # pylint: disable=old-style-class |
| 339 """Function currying implementation. |
| 340 |
| 341 Works for constructors too. Primary use is to be able to construct a class |
| 342 with some constructor arguments beings set ahead of actual initialization. |
| 343 Copy of an ASPN cookbook (#52549). |
| 344 """ |
| 345 |
| 346 def __init__(self, clazz, *args, **kwargs): |
| 347 self.clazz = clazz |
| 348 self.pending = args[:] |
| 349 self.kwargs = kwargs.copy() |
| 350 |
| 351 def __call__(self, *args, **kwargs): |
| 352 if kwargs and self.kwargs: |
| 353 kw = self.kwargs.copy() |
| 354 kw.update(kwargs) |
| 355 else: |
| 356 kw = kwargs or self.kwargs |
| 357 |
| 358 return self.clazz(*(self.pending + args), **kw) |
| 359 |
| 360 |
| 361 def Prepend(filepath, text): |
| 362 """ Prepends text to the file. |
| 363 |
| 364 Creates the file if it does not exist. |
| 365 """ |
| 366 file_data = text |
| 367 if os.path.exists(filepath): |
| 368 file_data += open(filepath).read() |
| 369 f = open(filepath, 'w') |
| 370 f.write(file_data) |
| 371 f.close() |
| 372 |
| 373 |
| 374 def MakeWorldReadable(path): |
| 375 """Change the permissions of the given path to make it world-readable. |
| 376 This is often needed for archived files, so they can be served by web servers |
| 377 or accessed by unprivileged network users.""" |
| 378 |
| 379 # No need to do anything special on Windows. |
| 380 if IsWindows(): |
| 381 return |
| 382 |
| 383 perms = stat.S_IMODE(os.stat(path)[stat.ST_MODE]) |
| 384 if os.path.isdir(path): |
| 385 # Directories need read and exec. |
| 386 os.chmod(path, perms | 0555) |
| 387 else: |
| 388 os.chmod(path, perms | 0444) |
| 389 |
| 390 |
| 391 def MakeParentDirectoriesWorldReadable(path): |
| 392 """Changes the permissions of the given path and its parent directories |
| 393 to make them world-readable. Stops on first directory which is |
| 394 world-readable. This is often needed for archive staging directories, |
| 395 so that they can be served by web servers or accessed by unprivileged |
| 396 network users.""" |
| 397 |
| 398 # No need to do anything special on Windows. |
| 399 if IsWindows(): |
| 400 return |
| 401 |
| 402 while path != os.path.dirname(path): |
| 403 current_permissions = stat.S_IMODE(os.stat(path)[stat.ST_MODE]) |
| 404 if current_permissions & 0555 == 0555: |
| 405 break |
| 406 os.chmod(path, current_permissions | 0555) |
| 407 path = os.path.dirname(path) |
| 408 |
| 409 |
| 410 def MaybeMakeDirectory(*path): |
| 411 """Creates an entire path, if it doesn't already exist.""" |
| 412 file_path = os.path.join(*path) |
| 413 try: |
| 414 os.makedirs(file_path) |
| 415 except OSError, e: |
| 416 if e.errno != errno.EEXIST: |
| 417 raise |
| 418 |
| 419 |
| 420 def RemovePath(*path): |
| 421 """Removes the file or directory at 'path', if it exists.""" |
| 422 file_path = os.path.join(*path) |
| 423 if os.path.exists(file_path): |
| 424 if os.path.isdir(file_path): |
| 425 RemoveDirectory(file_path) |
| 426 else: |
| 427 RemoveFile(file_path) |
| 428 |
| 429 |
| 430 def RemoveFile(*path): |
| 431 """Removes the file located at 'path', if it exists.""" |
| 432 file_path = os.path.join(*path) |
| 433 try: |
| 434 os.remove(file_path) |
| 435 except OSError, e: |
| 436 if e.errno != errno.ENOENT: |
| 437 raise |
| 438 |
| 439 |
| 440 def MoveFile(path, new_path): |
| 441 """Moves the file located at 'path' to 'new_path', if it exists.""" |
| 442 try: |
| 443 RemoveFile(new_path) |
| 444 os.rename(path, new_path) |
| 445 except OSError, e: |
| 446 if e.errno != errno.ENOENT: |
| 447 raise |
| 448 |
| 449 |
| 450 def LocateFiles(pattern, root=os.curdir): |
| 451 """Yeilds files matching pattern found in root and its subdirectories. |
| 452 |
| 453 An exception is thrown if root doesn't exist.""" |
| 454 for path, _, files in os.walk(os.path.abspath(root)): |
| 455 for filename in fnmatch.filter(files, pattern): |
| 456 yield os.path.join(path, filename) |
| 457 |
| 458 |
| 459 def RemoveFilesWildcards(file_wildcard, root=os.curdir): |
| 460 """Removes files matching 'file_wildcard' in root and its subdirectories, if |
| 461 any exists. |
| 462 |
| 463 An exception is thrown if root doesn't exist.""" |
| 464 for item in LocateFiles(file_wildcard, root): |
| 465 try: |
| 466 os.remove(item) |
| 467 except OSError, e: |
| 468 if e.errno != errno.ENOENT: |
| 469 raise |
| 470 |
| 471 |
| 472 def RemoveGlobbedPaths(path_wildcard, root=os.curdir): |
| 473 """Removes all paths matching 'path_wildcard' beneath root. |
| 474 |
| 475 Returns the list of paths removed. |
| 476 |
| 477 An exception is thrown if root doesn't exist.""" |
| 478 if not os.path.exists(root): |
| 479 raise OSError(2, 'No such file or directory', root) |
| 480 |
| 481 full_path_wildcard = os.path.join(path_wildcard, root) |
| 482 paths = glob.glob(full_path_wildcard) |
| 483 for path in paths: |
| 484 # When glob returns directories they end in "/." |
| 485 if path.endswith(os.sep + '.'): |
| 486 path = path[:-2] |
| 487 RemovePath(path) |
| 488 return paths |
| 489 |
| 490 |
| 491 def RemoveDirectory(*path): |
| 492 """Recursively removes a directory, even if it's marked read-only. |
| 493 |
| 494 Remove the directory located at *path, if it exists. |
| 495 |
| 496 shutil.rmtree() doesn't work on Windows if any of the files or directories |
| 497 are read-only, which svn repositories and some .svn files are. We need to |
| 498 be able to force the files to be writable (i.e., deletable) as we traverse |
| 499 the tree. |
| 500 |
| 501 Even with all this, Windows still sometimes fails to delete a file, citing |
| 502 a permission error (maybe something to do with antivirus scans or disk |
| 503 indexing). The best suggestion any of the user forums had was to wait a |
| 504 bit and try again, so we do that too. It's hand-waving, but sometimes it |
| 505 works. :/ |
| 506 """ |
| 507 file_path = os.path.join(*path) |
| 508 if not os.path.exists(file_path): |
| 509 return |
| 510 |
| 511 if sys.platform == 'win32': |
| 512 # Give up and use cmd.exe's rd command. |
| 513 file_path = os.path.normcase(file_path) |
| 514 for _ in xrange(3): |
| 515 print 'RemoveDirectory running %s' % (' '.join( |
| 516 ['cmd.exe', '/c', 'rd', '/q', '/s', file_path])) |
| 517 if not subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', file_path]): |
| 518 break |
| 519 print ' Failed' |
| 520 time.sleep(3) |
| 521 return |
| 522 |
| 523 def RemoveWithRetry_non_win(rmfunc, path): |
| 524 if os.path.islink(path): |
| 525 return os.remove(path) |
| 526 else: |
| 527 return rmfunc(path) |
| 528 |
| 529 remove_with_retry = RemoveWithRetry_non_win |
| 530 |
| 531 def RmTreeOnError(function, path, excinfo): |
| 532 r"""This works around a problem whereby python 2.x on Windows has no ability |
| 533 to check for symbolic links. os.path.islink always returns False. But |
| 534 shutil.rmtree will fail if invoked on a symbolic link whose target was |
| 535 deleted before the link. E.g., reproduce like this: |
| 536 > mkdir test |
| 537 > mkdir test\1 |
| 538 > mklink /D test\current test\1 |
| 539 > python -c "import chromium_utils; chromium_utils.RemoveDirectory('test')" |
| 540 To avoid this issue, we pass this error-handling function to rmtree. If |
| 541 we see the exact sort of failure, we ignore it. All other failures we re- |
| 542 raise. |
| 543 """ |
| 544 |
| 545 exception_type = excinfo[0] |
| 546 exception_value = excinfo[1] |
| 547 # If shutil.rmtree encounters a symbolic link on Windows, os.listdir will |
| 548 # fail with a WindowsError exception with an ENOENT errno (i.e., file not |
| 549 # found). We'll ignore that error. Note that WindowsError is not defined |
| 550 # for non-Windows platforms, so we use OSError (of which it is a subclass) |
| 551 # to avoid lint complaints about an undefined global on non-Windows |
| 552 # platforms. |
| 553 if (function is os.listdir) and issubclass(exception_type, OSError): |
| 554 if exception_value.errno == errno.ENOENT: |
| 555 # File does not exist, and we're trying to delete, so we can ignore the |
| 556 # failure. |
| 557 print 'WARNING: Failed to list %s during rmtree. Ignoring.\n' % path |
| 558 else: |
| 559 raise |
| 560 else: |
| 561 raise |
| 562 |
| 563 for root, dirs, files in os.walk(file_path, topdown=False): |
| 564 # For POSIX: making the directory writable guarantees removability. |
| 565 # Windows will ignore the non-read-only bits in the chmod value. |
| 566 os.chmod(root, 0770) |
| 567 for name in files: |
| 568 remove_with_retry(os.remove, os.path.join(root, name)) |
| 569 for name in dirs: |
| 570 remove_with_retry(lambda p: shutil.rmtree(p, onerror=RmTreeOnError), |
| 571 os.path.join(root, name)) |
| 572 |
| 573 remove_with_retry(os.rmdir, file_path) |
| 574 |
| 575 |
| 576 def CopyFileToDir(src_path, dest_dir, dest_fn=None, link_ok=False): |
| 577 """Copies the file found at src_path to the dest_dir directory, with metadata. |
| 578 |
| 579 If dest_fn is specified, the src_path is copied to that name in dest_dir, |
| 580 otherwise it is copied to a file of the same name. |
| 581 |
| 582 Raises PathNotFound if either the file or the directory is not found. |
| 583 """ |
| 584 # Verify the file and directory separately so we can tell them apart and |
| 585 # raise PathNotFound rather than shutil.copyfile's IOError. |
| 586 if not os.path.isfile(src_path): |
| 587 raise PathNotFound('Unable to find file %s' % src_path) |
| 588 if not os.path.isdir(dest_dir): |
| 589 raise PathNotFound('Unable to find dir %s' % dest_dir) |
| 590 src_file = os.path.basename(src_path) |
| 591 if dest_fn: |
| 592 # If we have ctypes and the caller doesn't mind links, use that to |
| 593 # try to make the copy faster on Windows. http://crbug.com/418702. |
| 594 if link_ok and WIN_LINK_FUNC: |
| 595 WIN_LINK_FUNC(src_path, os.path.join(dest_dir, dest_fn)) |
| 596 else: |
| 597 shutil.copy2(src_path, os.path.join(dest_dir, dest_fn)) |
| 598 else: |
| 599 shutil.copy2(src_path, os.path.join(dest_dir, src_file)) |
| 600 |
| 601 |
| 602 def MakeZip(output_dir, archive_name, file_list, file_relative_dir, |
| 603 raise_error=True, remove_archive_directory=True): |
| 604 """Packs files into a new zip archive. |
| 605 |
| 606 Files are first copied into a directory within the output_dir named for |
| 607 the archive_name, which will be created if necessary and emptied if it |
| 608 already exists. The files are then then packed using archive names |
| 609 relative to the output_dir. That is, if the zipfile is unpacked in place, |
| 610 it will create a directory identical to the new archive_name directory, in |
| 611 the output_dir. The zip file will be named as the archive_name, plus |
| 612 '.zip'. |
| 613 |
| 614 Args: |
| 615 output_dir: Absolute path to the directory in which the archive is to |
| 616 be created. |
| 617 archive_dir: Subdirectory of output_dir holding files to be added to |
| 618 the new zipfile. |
| 619 file_list: List of paths to files or subdirectories, relative to the |
| 620 file_relative_dir. |
| 621 file_relative_dir: Absolute path to the directory containing the files |
| 622 and subdirectories in the file_list. |
| 623 raise_error: Whether to raise a PathNotFound error if one of the files in |
| 624 the list is not found. |
| 625 remove_archive_directory: Whether to remove the archive staging directory |
| 626 before copying files over to it. |
| 627 |
| 628 Returns: |
| 629 A tuple consisting of (archive_dir, zip_file_path), where archive_dir |
| 630 is the full path to the newly created archive_name subdirectory. |
| 631 |
| 632 Raises: |
| 633 PathNotFound if any of the files in the list is not found, unless |
| 634 raise_error is False, in which case the error will be ignored. |
| 635 """ |
| 636 |
| 637 start_time = time.clock() |
| 638 # Collect files into the archive directory. |
| 639 archive_dir = os.path.join(output_dir, archive_name) |
| 640 print 'output_dir: %s, archive_name: %s' % (output_dir, archive_name) |
| 641 print 'archive_dir: %s, remove_archive_directory: %s, exists: %s' % ( |
| 642 archive_dir, remove_archive_directory, os.path.exists(archive_dir)) |
| 643 if remove_archive_directory and os.path.exists(archive_dir): |
| 644 # Move it even if it's not a directory as expected. This can happen with |
| 645 # FILES.cfg archive creation where we create an archive staging directory |
| 646 # that is the same name as the ultimate archive name. |
| 647 if not os.path.isdir(archive_dir): |
| 648 print 'Moving old "%s" file to create same name directory.' % archive_dir |
| 649 previous_archive_file = '%s.old' % archive_dir |
| 650 MoveFile(archive_dir, previous_archive_file) |
| 651 else: |
| 652 print 'Removing %s' % archive_dir |
| 653 RemoveDirectory(archive_dir) |
| 654 print 'Now, os.path.exists(%s): %s' % ( |
| 655 archive_dir, os.path.exists(archive_dir)) |
| 656 MaybeMakeDirectory(archive_dir) |
| 657 for needed_file in file_list: |
| 658 needed_file = needed_file.rstrip() |
| 659 # These paths are relative to the file_relative_dir. We need to copy |
| 660 # them over maintaining the relative directories, where applicable. |
| 661 src_path = os.path.join(file_relative_dir, needed_file) |
| 662 dirname, basename = os.path.split(needed_file) |
| 663 try: |
| 664 if os.path.isdir(src_path): |
| 665 if WIN_LINK_FUNC: |
| 666 WIN_LINK_FUNC(src_path, os.path.join(archive_dir, needed_file)) |
| 667 else: |
| 668 shutil.copytree(src_path, os.path.join(archive_dir, needed_file), |
| 669 symlinks=True) |
| 670 elif dirname != '' and basename != '': |
| 671 dest_dir = os.path.join(archive_dir, dirname) |
| 672 MaybeMakeDirectory(dest_dir) |
| 673 CopyFileToDir(src_path, dest_dir, basename, link_ok=True) |
| 674 else: |
| 675 CopyFileToDir(src_path, archive_dir, basename, link_ok=True) |
| 676 except PathNotFound: |
| 677 if raise_error: |
| 678 raise |
| 679 end_time = time.clock() |
| 680 print 'Took %f seconds to create archive directory.' % (end_time - start_time) |
| 681 |
| 682 # Pack the zip file. |
| 683 output_file = '%s.zip' % archive_dir |
| 684 previous_file = '%s_old.zip' % archive_dir |
| 685 MoveFile(output_file, previous_file) |
| 686 |
| 687 # If we have 7z, use that as it's much faster. See http://crbug.com/418702. |
| 688 windows_zip_cmd = None |
| 689 if os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'): |
| 690 windows_zip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'a', '-y', '-mx1'] |
| 691 |
| 692 # On Windows we use the python zip module; on Linux and Mac, we use the zip |
| 693 # command as it will handle links and file bits (executable). Which is much |
| 694 # easier then trying to do that with ZipInfo options. |
| 695 start_time = time.clock() |
| 696 if IsWindows() and not windows_zip_cmd: |
| 697 print 'Creating %s' % output_file |
| 698 |
| 699 def _Addfiles(to_zip_file, dirname, files_to_add): |
| 700 for this_file in files_to_add: |
| 701 archive_name = this_file |
| 702 this_path = os.path.join(dirname, this_file) |
| 703 if os.path.isfile(this_path): |
| 704 # Store files named relative to the outer output_dir. |
| 705 archive_name = this_path.replace(output_dir + os.sep, '') |
| 706 if os.path.getsize(this_path) == 0: |
| 707 compress_method = zipfile.ZIP_STORED |
| 708 else: |
| 709 compress_method = zipfile.ZIP_DEFLATED |
| 710 to_zip_file.write(this_path, archive_name, compress_method) |
| 711 print 'Adding %s' % archive_name |
| 712 zip_file = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED, |
| 713 allowZip64=True) |
| 714 try: |
| 715 os.path.walk(archive_dir, _Addfiles, zip_file) |
| 716 finally: |
| 717 zip_file.close() |
| 718 else: |
| 719 if IsMac() or IsLinux(): |
| 720 zip_cmd = ['zip', '-yr1'] |
| 721 else: |
| 722 zip_cmd = windows_zip_cmd |
| 723 saved_dir = os.getcwd() |
| 724 os.chdir(os.path.dirname(archive_dir)) |
| 725 command = zip_cmd + [output_file, os.path.basename(archive_dir)] |
| 726 result = RunCommand(command) |
| 727 os.chdir(saved_dir) |
| 728 if result and raise_error: |
| 729 raise ExternalError('zip failed: %s => %s' % |
| 730 (str(command), result)) |
| 731 end_time = time.clock() |
| 732 print 'Took %f seconds to create zip.' % (end_time - start_time) |
| 733 return (archive_dir, output_file) |
| 734 |
| 735 |
| 736 def ExtractZip(filename, output_dir, verbose=True): |
| 737 """ Extract the zip archive in the output directory. |
| 738 """ |
| 739 MaybeMakeDirectory(output_dir) |
| 740 |
| 741 # On Linux and Mac, we use the unzip command as it will |
| 742 # handle links and file bits (executable), which is much |
| 743 # easier then trying to do that with ZipInfo options. |
| 744 # |
| 745 # The Mac Version of unzip unfortunately does not support Zip64, whereas |
| 746 # the python module does, so we have to fallback to the python zip module |
| 747 # on Mac if the filesize is greater than 4GB. |
| 748 # |
| 749 # On Windows, try to use 7z if it is installed, otherwise fall back to python |
| 750 # zip module and pray we don't have files larger than 512MB to unzip. |
| 751 unzip_cmd = None |
| 752 if ((IsMac() and os.path.getsize(filename) < 4 * 1024 * 1024 * 1024) |
| 753 or IsLinux()): |
| 754 unzip_cmd = ['unzip', '-o'] |
| 755 elif IsWindows() and os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'): |
| 756 unzip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y'] |
| 757 |
| 758 if unzip_cmd: |
| 759 # Make sure path is absolute before changing directories. |
| 760 filepath = os.path.abspath(filename) |
| 761 saved_dir = os.getcwd() |
| 762 os.chdir(output_dir) |
| 763 command = unzip_cmd + [filepath] |
| 764 result = RunCommand(command) |
| 765 os.chdir(saved_dir) |
| 766 if result: |
| 767 raise ExternalError('unzip failed: %s => %s' % (str(command), result)) |
| 768 else: |
| 769 assert IsWindows() or IsMac() |
| 770 zf = zipfile.ZipFile(filename) |
| 771 # TODO(hinoka): This can be multiprocessed. |
| 772 for name in zf.namelist(): |
| 773 if verbose: |
| 774 print 'Extracting %s' % name |
| 775 zf.extract(name, output_dir) |
| 776 if IsMac(): |
| 777 # Restore permission bits. |
| 778 os.chmod(os.path.join(output_dir, name), |
| 779 zf.getinfo(name).external_attr >> 16L) |
| 780 |
| 781 |
| 782 def WindowsPath(path): |
| 783 """Returns a Windows mixed-style absolute path, given a Cygwin absolute path. |
| 784 |
| 785 The version of Python in the Chromium tree uses posixpath for os.path even |
| 786 on Windows, so we convert to a mixed Windows path (that is, a Windows path |
| 787 that uses forward slashes instead of backslashes) manually. |
| 788 """ |
| 789 # TODO(pamg): make this work for other drives too. |
| 790 if path.startswith('/cygdrive/c/'): |
| 791 return path.replace('/cygdrive/c/', 'C:/') |
| 792 return path |
| 793 |
| 794 |
| 795 def FindUpwardParent(start_dir, *desired_list): |
| 796 """Finds the desired object's parent, searching upward from the start_dir. |
| 797 |
| 798 Searches within start_dir and within all its parents looking for the desired |
| 799 directory or file, which may be given in one or more path components. Returns |
| 800 the first directory in which the top desired path component was found, or |
| 801 raises PathNotFound if it wasn't. |
| 802 """ |
| 803 desired_path = os.path.join(*desired_list) |
| 804 last_dir = '' |
| 805 cur_dir = start_dir |
| 806 found_path = os.path.join(cur_dir, desired_path) |
| 807 while not os.path.exists(found_path): |
| 808 last_dir = cur_dir |
| 809 cur_dir = os.path.dirname(cur_dir) |
| 810 if last_dir == cur_dir: |
| 811 raise PathNotFound('Unable to find %s above %s' % |
| 812 (desired_path, start_dir)) |
| 813 found_path = os.path.join(cur_dir, desired_path) |
| 814 # Strip the entire original desired path from the end of the one found |
| 815 # and remove a trailing path separator, if present. |
| 816 found_path = found_path[:len(found_path) - len(desired_path)] |
| 817 if found_path.endswith(os.sep): |
| 818 found_path = found_path[:len(found_path) - 1] |
| 819 return found_path |
| 820 |
| 821 |
| 822 def FindUpward(start_dir, *desired_list): |
| 823 """Returns a path to the desired directory or file, searching upward. |
| 824 |
| 825 Searches within start_dir and within all its parents looking for the desired |
| 826 directory or file, which may be given in one or more path components. Returns |
| 827 the full path to the desired object, or raises PathNotFound if it wasn't |
| 828 found. |
| 829 """ |
| 830 parent = FindUpwardParent(start_dir, *desired_list) |
| 831 return os.path.join(parent, *desired_list) |
| 832 |
| 833 |
| 834 def RunAndPrintDots(function): |
| 835 """Starts a background thread that prints dots while the function runs.""" |
| 836 |
| 837 def Hook(*args, **kwargs): |
| 838 event = threading.Event() |
| 839 |
| 840 def PrintDots(): |
| 841 counter = 0 |
| 842 while not event.isSet(): |
| 843 event.wait(5) |
| 844 sys.stdout.write('.') |
| 845 counter = (counter + 1) % 80 |
| 846 if not counter: |
| 847 sys.stdout.write('\n') |
| 848 sys.stdout.flush() |
| 849 t = threading.Thread(target=PrintDots) |
| 850 t.start() |
| 851 try: |
| 852 return function(*args, **kwargs) |
| 853 finally: |
| 854 event.set() |
| 855 t.join() |
| 856 return Hook |
| 857 |
| 858 |
| 859 class RunCommandFilter(object): |
| 860 """Class that should be subclassed to provide a filter for RunCommand.""" |
| 861 # Method could be a function |
| 862 # pylint: disable=R0201 |
| 863 |
| 864 def FilterLine(self, a_line): |
| 865 """Called for each line of input. The \n is included on a_line. Should |
| 866 return what is to be recorded as the output for this line. A result of |
| 867 None suppresses the line.""" |
| 868 return a_line |
| 869 |
| 870 def FilterDone(self, last_bits): |
| 871 """Acts just like FilterLine, but is called with any data collected after |
| 872 the last newline of the command.""" |
| 873 return last_bits |
| 874 |
| 875 |
| 876 class FilterCapture(RunCommandFilter): |
| 877 """Captures the text and places it into an array.""" |
| 878 def __init__(self): |
| 879 RunCommandFilter.__init__(self) |
| 880 self.text = [] |
| 881 |
| 882 def FilterLine(self, line): |
| 883 self.text.append(line.rstrip()) |
| 884 |
| 885 def FilterDone(self, text): |
| 886 self.text.append(text) |
| 887 |
| 888 |
| 889 def RunCommand(command, parser_func=None, filter_obj=None, pipes=None, |
| 890 print_cmd=True, timeout=None, max_time=None, **kwargs): |
| 891 """Runs the command list, printing its output and returning its exit status. |
| 892 |
| 893 Prints the given command (which should be a list of one or more strings), |
| 894 then runs it and writes its stdout and stderr to the appropriate file handles. |
| 895 |
| 896 If timeout is set, the process will be killed if output is stopped after |
| 897 timeout seconds. If max_time is set, the process will be killed if it runs for |
| 898 more than max_time. |
| 899 |
| 900 If parser_func is not given, the subprocess's output is passed to stdout |
| 901 and stderr directly. If the func is given, each line of the subprocess's |
| 902 stdout/stderr is passed to the func and then written to stdout. |
| 903 |
| 904 If filter_obj is given, all output is run through the filter a line |
| 905 at a time before it is written to stdout. |
| 906 |
| 907 We do not currently support parsing stdout and stderr independent of |
| 908 each other. In previous attempts, this led to output ordering issues. |
| 909 By merging them when either needs to be parsed, we avoid those ordering |
| 910 issues completely. |
| 911 |
| 912 pipes is a list of commands (also a list) that will receive the output of |
| 913 the intial command. For example, if you want to run "python a | python b | c", |
| 914 the "command" will be set to ['python', 'a'], while pipes will be set to |
| 915 [['python', 'b'],['c']] |
| 916 """ |
| 917 |
| 918 def TimedFlush(timeout, fh, kill_event): |
| 919 """Flush fh every timeout seconds until kill_event is true.""" |
| 920 while True: |
| 921 try: |
| 922 fh.flush() |
| 923 # File handle is closed, exit. |
| 924 except ValueError: |
| 925 break |
| 926 # Wait for kill signal or timeout. |
| 927 if kill_event.wait(timeout): |
| 928 break |
| 929 |
| 930 # TODO(all): nsylvain's CommandRunner in buildbot_slave is based on this |
| 931 # method. Update it when changes are introduced here. |
| 932 def ProcessRead(readfh, writefh, parser_func=None, filter_obj=None, |
| 933 log_event=None): |
| 934 writefh.flush() |
| 935 |
| 936 # Python on Windows writes the buffer only when it reaches 4k. Ideally |
| 937 # we would flush a minimum of 10 seconds. However, we only write and |
| 938 # flush no more often than 20 seconds to avoid flooding the master with |
| 939 # network traffic from unbuffered output. |
| 940 kill_event = threading.Event() |
| 941 flush_thread = threading.Thread( |
| 942 target=TimedFlush, args=(20, writefh, kill_event)) |
| 943 flush_thread.daemon = True |
| 944 flush_thread.start() |
| 945 |
| 946 try: |
| 947 in_byte = readfh.read(1) |
| 948 in_line = cStringIO.StringIO() |
| 949 while in_byte: |
| 950 # Capture all characters except \r. |
| 951 if in_byte != '\r': |
| 952 in_line.write(in_byte) |
| 953 |
| 954 # Write and flush on newline. |
| 955 if in_byte == '\n': |
| 956 if log_event: |
| 957 log_event.set() |
| 958 if parser_func: |
| 959 parser_func(in_line.getvalue().strip()) |
| 960 |
| 961 if filter_obj: |
| 962 filtered_line = filter_obj.FilterLine(in_line.getvalue()) |
| 963 if filtered_line is not None: |
| 964 writefh.write(filtered_line) |
| 965 else: |
| 966 writefh.write(in_line.getvalue()) |
| 967 in_line = cStringIO.StringIO() |
| 968 in_byte = readfh.read(1) |
| 969 |
| 970 if log_event and in_line.getvalue(): |
| 971 log_event.set() |
| 972 |
| 973 # Write remaining data and flush on EOF. |
| 974 if parser_func: |
| 975 parser_func(in_line.getvalue().strip()) |
| 976 |
| 977 if filter_obj: |
| 978 if in_line.getvalue(): |
| 979 filtered_line = filter_obj.FilterDone(in_line.getvalue()) |
| 980 if filtered_line is not None: |
| 981 writefh.write(filtered_line) |
| 982 else: |
| 983 if in_line.getvalue(): |
| 984 writefh.write(in_line.getvalue()) |
| 985 finally: |
| 986 kill_event.set() |
| 987 flush_thread.join() |
| 988 writefh.flush() |
| 989 |
| 990 pipes = pipes or [] |
| 991 |
| 992 # Print the given command (which should be a list of one or more strings). |
| 993 if print_cmd: |
| 994 print '\n' + subprocess.list2cmdline(command) + '\n', |
| 995 for pipe in pipes: |
| 996 print ' | ' + subprocess.list2cmdline(pipe) + '\n', |
| 997 |
| 998 sys.stdout.flush() |
| 999 sys.stderr.flush() |
| 1000 |
| 1001 if not (parser_func or filter_obj or pipes or timeout or max_time): |
| 1002 # Run the command. The stdout and stderr file handles are passed to the |
| 1003 # subprocess directly for writing. No processing happens on the output of |
| 1004 # the subprocess. |
| 1005 proc = subprocess.Popen(command, stdout=sys.stdout, stderr=sys.stderr, |
| 1006 bufsize=0, **kwargs) |
| 1007 |
| 1008 else: |
| 1009 if not (parser_func or filter_obj): |
| 1010 filter_obj = RunCommandFilter() |
| 1011 |
| 1012 # Start the initial process. |
| 1013 proc = subprocess.Popen(command, stdout=subprocess.PIPE, |
| 1014 stderr=subprocess.STDOUT, bufsize=0, **kwargs) |
| 1015 proc_handles = [proc] |
| 1016 |
| 1017 if pipes: |
| 1018 pipe_number = 0 |
| 1019 for pipe in pipes: |
| 1020 pipe_number = pipe_number + 1 |
| 1021 if pipe_number == len(pipes) and not (parser_func or filter_obj): |
| 1022 # The last pipe process needs to output to sys.stdout or filter |
| 1023 stdout = sys.stdout |
| 1024 else: |
| 1025 # Output to a pipe, since another pipe is on top of us. |
| 1026 stdout = subprocess.PIPE |
| 1027 pipe_proc = subprocess.Popen(pipe, stdin=proc_handles[0].stdout, |
| 1028 stdout=stdout, stderr=subprocess.STDOUT) |
| 1029 proc_handles.insert(0, pipe_proc) |
| 1030 |
| 1031 # Allow proc to receive a SIGPIPE if the piped process exits. |
| 1032 for handle in proc_handles[1:]: |
| 1033 handle.stdout.close() |
| 1034 |
| 1035 log_event = threading.Event() |
| 1036 |
| 1037 # Launch and start the reader thread. |
| 1038 thread = threading.Thread(target=ProcessRead, |
| 1039 args=(proc_handles[0].stdout, sys.stdout), |
| 1040 kwargs={'parser_func': parser_func, |
| 1041 'filter_obj': filter_obj, |
| 1042 'log_event': log_event}) |
| 1043 |
| 1044 kill_lock = threading.Lock() |
| 1045 |
| 1046 |
| 1047 def term_then_kill(handle, initial_timeout, numtimeouts, interval): |
| 1048 def timed_check(): |
| 1049 for _ in range(numtimeouts): |
| 1050 if handle.poll() is not None: |
| 1051 return True |
| 1052 time.sleep(interval) |
| 1053 |
| 1054 handle.terminate() |
| 1055 time.sleep(initial_timeout) |
| 1056 timed_check() |
| 1057 if handle.poll() is None: |
| 1058 handle.kill() |
| 1059 timed_check() |
| 1060 return handle.poll() is not None |
| 1061 |
| 1062 |
| 1063 def kill_proc(proc_handles, message=None): |
| 1064 with kill_lock: |
| 1065 if proc_handles: |
| 1066 killed = term_then_kill(proc_handles[0], 0.1, 5, 1) |
| 1067 |
| 1068 if message: |
| 1069 print >> sys.stderr, message |
| 1070 |
| 1071 if not killed: |
| 1072 print >> sys.stderr, 'could not kill pid %d!' % proc_handles[0].pid |
| 1073 else: |
| 1074 print >> sys.stderr, 'program finished with exit code %d' % ( |
| 1075 proc_handles[0].returncode) |
| 1076 |
| 1077 # Prevent other timeouts from double-killing. |
| 1078 del proc_handles[:] |
| 1079 |
| 1080 def timeout_func(timeout, proc_handles, log_event, finished_event): |
| 1081 while log_event.wait(timeout): |
| 1082 log_event.clear() |
| 1083 if finished_event.is_set(): |
| 1084 return |
| 1085 |
| 1086 message = ('command timed out: %d seconds without output, attempting to ' |
| 1087 'kill' % timeout) |
| 1088 kill_proc(proc_handles, message) |
| 1089 |
| 1090 def maxtimeout_func(timeout, proc_handles, finished_event): |
| 1091 if not finished_event.wait(timeout): |
| 1092 message = ('command timed out: %d seconds elapsed' % timeout) |
| 1093 kill_proc(proc_handles, message) |
| 1094 |
| 1095 timeout_thread = None |
| 1096 maxtimeout_thread = None |
| 1097 finished_event = threading.Event() |
| 1098 |
| 1099 if timeout: |
| 1100 timeout_thread = threading.Thread(target=timeout_func, |
| 1101 args=(timeout, proc_handles, log_event, |
| 1102 finished_event)) |
| 1103 timeout_thread.daemon = True |
| 1104 if max_time: |
| 1105 maxtimeout_thread = threading.Thread(target=maxtimeout_func, |
| 1106 args=(max_time, proc_handles, |
| 1107 finished_event)) |
| 1108 maxtimeout_thread.daemon = True |
| 1109 |
| 1110 thread.start() |
| 1111 if timeout_thread: |
| 1112 timeout_thread.start() |
| 1113 if maxtimeout_thread: |
| 1114 maxtimeout_thread.start() |
| 1115 |
| 1116 # Wait for the commands to terminate. |
| 1117 for handle in proc_handles: |
| 1118 handle.wait() |
| 1119 |
| 1120 # Wake up timeout threads. |
| 1121 finished_event.set() |
| 1122 log_event.set() |
| 1123 |
| 1124 # Wait for the reader thread to complete (implies EOF reached on stdout/ |
| 1125 # stderr pipes). |
| 1126 thread.join() |
| 1127 |
| 1128 # Check whether any of the sub commands has failed. |
| 1129 for handle in proc_handles: |
| 1130 if handle.returncode: |
| 1131 return handle.returncode |
| 1132 |
| 1133 # Wait for the command to terminate. |
| 1134 proc.wait() |
| 1135 return proc.returncode |
| 1136 |
| 1137 |
| 1138 def GetStatusOutput(command, **kwargs): |
| 1139 """Runs the command list, returning its result and output.""" |
| 1140 proc = subprocess.Popen(command, stdout=subprocess.PIPE, |
| 1141 stderr=subprocess.STDOUT, bufsize=1, |
| 1142 **kwargs) |
| 1143 output = proc.communicate()[0] |
| 1144 result = proc.returncode |
| 1145 |
| 1146 return (result, output) |
| 1147 |
| 1148 |
| 1149 def GetCommandOutput(command): |
| 1150 """Runs the command list, returning its output. |
| 1151 |
| 1152 Run the command and returns its output (stdout and stderr) as a string. |
| 1153 |
| 1154 If the command exits with an error, raises ExternalError. |
| 1155 """ |
| 1156 (result, output) = GetStatusOutput(command) |
| 1157 if result: |
| 1158 raise ExternalError('%s: %s' % (subprocess.list2cmdline(command), output)) |
| 1159 return output |
| 1160 |
| 1161 |
| 1162 def GetGClientCommand(platform=None): |
| 1163 """Returns the executable command name, depending on the platform. |
| 1164 """ |
| 1165 if not platform: |
| 1166 platform = sys.platform |
| 1167 if platform.startswith('win'): |
| 1168 # Windows doesn't want to depend on bash. |
| 1169 return 'gclient.bat' |
| 1170 else: |
| 1171 return 'gclient' |
| 1172 |
| 1173 |
| 1174 # Linux scripts use ssh to to move files to the archive host. |
| 1175 def SshMakeDirectory(host, dest_path): |
| 1176 """Creates the entire dest_path on the remote ssh host. |
| 1177 """ |
| 1178 command = ['ssh', host, 'mkdir', '-p', dest_path] |
| 1179 result = RunCommand(command) |
| 1180 if result: |
| 1181 raise ExternalError('Failed to ssh mkdir "%s" on "%s" (%s)' % |
| 1182 (dest_path, host, result)) |
| 1183 |
| 1184 |
| 1185 def SshMoveFile(host, src_path, dest_path): |
| 1186 """Moves src_path (if it exists) to dest_path on the remote host. |
| 1187 """ |
| 1188 command = ['ssh', host, 'test', '-e', src_path] |
| 1189 result = RunCommand(command) |
| 1190 if result: |
| 1191 # Nothing to do if src_path doesn't exist. |
| 1192 return result |
| 1193 |
| 1194 command = ['ssh', host, 'mv', src_path, dest_path] |
| 1195 result = RunCommand(command) |
| 1196 if result: |
| 1197 raise ExternalError('Failed to ssh mv "%s" -> "%s" on "%s" (%s)' % |
| 1198 (src_path, dest_path, host, result)) |
| 1199 |
| 1200 |
| 1201 def SshCopyFiles(srcs, host, dst): |
| 1202 """Copies the srcs file(s) to dst on the remote ssh host. |
| 1203 dst is expected to exist. |
| 1204 """ |
| 1205 command = ['scp', srcs, host + ':' + dst] |
| 1206 result = RunCommand(command) |
| 1207 if result: |
| 1208 raise ExternalError('Failed to scp "%s" to "%s" (%s)' % |
| 1209 (srcs, host + ':' + dst, result)) |
| 1210 |
| 1211 |
| 1212 def SshExtractZip(host, zipname, dst): |
| 1213 """extract the remote zip file to dst on the remote ssh host. |
| 1214 """ |
| 1215 command = ['ssh', host, 'unzip', '-o', '-d', dst, zipname] |
| 1216 result = RunCommand(command) |
| 1217 if result: |
| 1218 raise ExternalError('Failed to ssh unzip -o -d "%s" "%s" on "%s" (%s)' % |
| 1219 (dst, zipname, host, result)) |
| 1220 |
| 1221 # unzip will create directories with access 700, which is not often what we |
| 1222 # need. Fix the permissions for the whole archive. |
| 1223 command = ['ssh', host, 'chmod', '-R', '755', dst] |
| 1224 result = RunCommand(command) |
| 1225 if result: |
| 1226 raise ExternalError('Failed to ssh chmod -R 755 "%s" on "%s" (%s)' % |
| 1227 (dst, host, result)) |
| 1228 |
| 1229 |
| 1230 def SshCopyTree(srctree, host, dst): |
| 1231 """Recursively copies the srctree to dst on the remote ssh host. |
| 1232 For consistency with shutil, dst is expected to not exist. |
| 1233 """ |
| 1234 command = ['ssh', host, '[ -d "%s" ]' % dst] |
| 1235 result = RunCommand(command) |
| 1236 if result: |
| 1237 raise ExternalError('SshCopyTree destination directory "%s" already exists.' |
| 1238 % host + ':' + dst) |
| 1239 |
| 1240 SshMakeDirectory(host, os.path.dirname(dst)) |
| 1241 command = ['scp', '-r', '-p', srctree, host + ':' + dst] |
| 1242 result = RunCommand(command) |
| 1243 if result: |
| 1244 raise ExternalError('Failed to scp "%s" to "%s" (%s)' % |
| 1245 (srctree, host + ':' + dst, result)) |
| 1246 |
| 1247 |
| 1248 def ListMasters(cue='master.cfg', include_public=True, include_internal=True): |
| 1249 """Returns all the masters found.""" |
| 1250 # Look for "internal" masters first. |
| 1251 path_internal = os.path.join( |
| 1252 BUILD_DIR, os.pardir, 'build_internal', 'masters/*/' + cue) |
| 1253 path = os.path.join(BUILD_DIR, 'masters/*/' + cue) |
| 1254 filenames = [] |
| 1255 if include_public: |
| 1256 filenames += glob.glob(path) |
| 1257 if include_internal: |
| 1258 filenames += glob.glob(path_internal) |
| 1259 return [os.path.abspath(os.path.dirname(f)) for f in filenames] |
| 1260 |
| 1261 |
| 1262 def MasterPath(mastername, include_public=True, include_internal=True): |
| 1263 path = os.path.join(BUILD_DIR, 'masters', 'master.%s' % mastername) |
| 1264 path_internal = os.path.join( |
| 1265 BUILD_DIR, os.pardir, 'build_internal', 'masters', |
| 1266 'master.%s' % mastername) |
| 1267 if include_public and os.path.isdir(path): |
| 1268 return path |
| 1269 if include_internal and os.path.isdir(path_internal): |
| 1270 return path_internal |
| 1271 raise LookupError('Path for master %s not found' % mastername) |
| 1272 |
| 1273 |
| 1274 def ListMastersWithSlaves(include_public=True, include_internal=True): |
| 1275 masters_path = ListMasters('builders.pyl', include_public, include_internal) |
| 1276 masters_path.extend(ListMasters('slaves.cfg', include_public, |
| 1277 include_internal)) |
| 1278 return masters_path |
| 1279 |
| 1280 |
| 1281 def GetSlavesFromMasterPath(path, fail_hard=False): |
| 1282 builders_path = os.path.join(path, 'builders.pyl') |
| 1283 if os.path.exists(builders_path): |
| 1284 return GetSlavesFromBuildersFile(builders_path) |
| 1285 return RunSlavesCfg(os.path.join(path, 'slaves.cfg'), fail_hard=fail_hard) |
| 1286 |
| 1287 |
| 1288 def GetAllSlaves(fail_hard=False, include_public=True, include_internal=True): |
| 1289 """Return all slave objects from masters.""" |
| 1290 slaves = [] |
| 1291 for master in ListMastersWithSlaves(include_public, include_internal): |
| 1292 cur_slaves = GetSlavesFromMasterPath(master, fail_hard) |
| 1293 for slave in cur_slaves: |
| 1294 slave['mastername'] = os.path.basename(master) |
| 1295 slaves.extend(cur_slaves) |
| 1296 return slaves |
| 1297 |
| 1298 |
| 1299 def GetSlavesForHost(): |
| 1300 """Get slaves for a host, defaulting to current host.""" |
| 1301 hostname = os.getenv('TESTING_SLAVENAME') |
| 1302 if not hostname: |
| 1303 hostname = socket.getfqdn().split('.', 1)[0].lower() |
| 1304 return [s for s in GetAllSlaves() if s.get('hostname') == hostname] |
| 1305 |
| 1306 |
| 1307 def GetActiveSubdir(): |
| 1308 """Get current checkout's subdir, if checkout uses subdir layout.""" |
| 1309 rootdir, subdir = os.path.split(os.path.dirname(BUILD_DIR)) |
| 1310 if subdir != 'b' and os.path.basename(rootdir) == 'c': |
| 1311 return subdir |
| 1312 |
| 1313 |
| 1314 def GetActiveSlavename(): |
| 1315 slavename = os.getenv('TESTING_SLAVENAME') |
| 1316 if not slavename: |
| 1317 slavename = socket.getfqdn().split('.', 1)[0].lower() |
| 1318 subdir = GetActiveSubdir() |
| 1319 if subdir: |
| 1320 return '%s#%s' % (slavename, subdir) |
| 1321 return slavename |
| 1322 |
| 1323 |
| 1324 def EntryToSlaveName(entry): |
| 1325 """Produces slave name from the slaves config dict.""" |
| 1326 name = entry.get('slavename') or entry.get('hostname') |
| 1327 if 'subdir' in entry: |
| 1328 return '%s#%s' % (name, entry['subdir']) |
| 1329 return name |
| 1330 |
| 1331 |
| 1332 def GetActiveMaster(slavename=None, default=None): |
| 1333 """Returns the name of the Active master serving the current host. |
| 1334 |
| 1335 Parse all of the active masters with slaves matching the current hostname |
| 1336 and optional slavename. Returns |default| if no match found. |
| 1337 """ |
| 1338 slavename = slavename or GetActiveSlavename() |
| 1339 for slave in GetAllSlaves(): |
| 1340 if slavename == EntryToSlaveName(slave): |
| 1341 return slave['master'] |
| 1342 return default |
| 1343 |
| 1344 |
| 1345 @contextmanager |
| 1346 def MasterEnvironment(master_dir): |
| 1347 """Context manager that enters an enviornment similar to a master's. |
| 1348 |
| 1349 This involves: |
| 1350 - Modifying 'sys.path' to include paths available to the master. |
| 1351 - Changing directory (via os.chdir()) to the master's base directory. |
| 1352 |
| 1353 These changes will be reverted after the context manager completes. |
| 1354 |
| 1355 Args: |
| 1356 master_dir: (str) The master's base directory. |
| 1357 """ |
| 1358 master_dir = os.path.abspath(master_dir) |
| 1359 |
| 1360 # Setup a 'sys.path' that is adequate for loading 'slaves.cfg'. |
| 1361 old_cwd = os.getcwd() |
| 1362 |
| 1363 with env.GetInfraPythonPath(master_dir=master_dir).Enter(): |
| 1364 try: |
| 1365 os.chdir(master_dir) |
| 1366 yield |
| 1367 finally: |
| 1368 os.chdir(old_cwd) |
| 1369 |
| 1370 |
| 1371 def ParsePythonCfg(cfg_filepath, fail_hard=False): |
| 1372 """Retrieves data from a python config file.""" |
| 1373 if not os.path.exists(cfg_filepath): |
| 1374 return None |
| 1375 |
| 1376 # Execute 'slaves.sfg' in the master path environment. |
| 1377 with MasterEnvironment(os.path.dirname(os.path.abspath(cfg_filepath))): |
| 1378 try: |
| 1379 local_vars = {} |
| 1380 execfile(os.path.join(cfg_filepath), local_vars) |
| 1381 del local_vars['__builtins__'] |
| 1382 return local_vars |
| 1383 except Exception as e: |
| 1384 # pylint: disable=C0323 |
| 1385 print >>sys.stderr, 'An error occurred while parsing %s: %s' % ( |
| 1386 cfg_filepath, e) |
| 1387 print >>sys.stderr, traceback.format_exc() # pylint: disable=C0323 |
| 1388 if fail_hard: |
| 1389 raise |
| 1390 return {} |
| 1391 |
| 1392 |
| 1393 def RunSlavesCfg(slaves_cfg, fail_hard=False): |
| 1394 """Runs slaves.cfg in a consistent way.""" |
| 1395 slave_config = ParsePythonCfg(slaves_cfg, fail_hard=fail_hard) or {} |
| 1396 return slave_config.get('slaves', []) |
| 1397 |
| 1398 |
| 1399 def convert_json(option, _, value, parser): |
| 1400 """Provide an OptionParser callback to unmarshal a JSON string.""" |
| 1401 setattr(parser.values, option.dest, json.loads(value)) |
| 1402 |
| 1403 |
| 1404 def b64_gz_json_encode(obj): |
| 1405 """Serialize a python object into base64.""" |
| 1406 # The |separators| argument is to densify the command line. |
| 1407 return base64.b64encode(zlib.compress( |
| 1408 json.dumps(obj or {}, sort_keys=True, separators=(',', ':')), 9)) |
| 1409 |
| 1410 |
| 1411 def convert_gz_json(option, _, value, parser): |
| 1412 """Provide an OptionParser callback to unmarshal a b64 gz JSON string.""" |
| 1413 setattr( |
| 1414 parser.values, option.dest, |
| 1415 json.loads(zlib.decompress(base64.b64decode(value)))) |
| 1416 |
| 1417 |
| 1418 def SafeTranslate(inputstr): |
| 1419 """Convert a free form string to one that can be used in a path. |
| 1420 |
| 1421 This is similar to the safeTranslate function in buildbot. |
| 1422 """ |
| 1423 |
| 1424 badchars_map = string.maketrans('\t !#$%&\'()*+,./:;<=>?@[\\]^{|}~', |
| 1425 '______________________________') |
| 1426 if isinstance(inputstr, unicode): |
| 1427 inputstr = inputstr.encode('utf8') |
| 1428 return inputstr.translate(badchars_map) |
| 1429 |
| 1430 |
| 1431 def GetPrimaryProject(options): |
| 1432 """Returns: (str) the key of the primary project, or 'None' if none exists. |
| 1433 """ |
| 1434 # The preferred way is to reference the 'primary_project' parameter. |
| 1435 result = options.build_properties.get('primary_project') |
| 1436 if result: |
| 1437 return result |
| 1438 |
| 1439 # TODO(dnj): The 'primary_repo' parameter is used by some scripts to indictate |
| 1440 # the primary project name. This is not consistently used and will be |
| 1441 # deprecated in favor of 'primary_project' once that is rolled out. |
| 1442 result = options.build_properties.get('primary_repo') |
| 1443 if not result: |
| 1444 # The 'primary_repo' property currently contains a trailing underscore. |
| 1445 # However, this isn't an obvious thing given its name, so we'll strip it |
| 1446 # here and remove that expectation. |
| 1447 return result.strip('_') |
| 1448 return None |
| 1449 |
| 1450 |
| 1451 def GetBuildSortKey(options, project=None): |
| 1452 """Reads a variety of sources to determine the current build revision. |
| 1453 |
| 1454 NOTE: Currently, the return value does not qualify branch name. This can |
| 1455 present a problem with git numbering scheme, where numbers are only unique |
| 1456 in the context of their respective branches. When this happens, this |
| 1457 function will return a branch name as part of the sort key and its callers |
| 1458 will need to adapt their naming/querying schemes to accommodate this. Until |
| 1459 then, we will return 'None' as the branch name. |
| 1460 (e.g., refs/foo/bar@{#12345} => ("refs/foo/bar", 12345) |
| 1461 |
| 1462 Args: |
| 1463 options: Command-line options structure |
| 1464 project: (str/None) If not None, the project to get the build sort key |
| 1465 for. Otherwise, the build-wide sort key will be used. |
| 1466 Returns: (branch, value) The qualified sortkey value |
| 1467 branch: (str/None) The name of the branch, or 'None' if there is no branch |
| 1468 context. Currently this always returns 'None'. |
| 1469 value: (int) The iteration value within the specified branch |
| 1470 Raises: (NoIdentifiedRevision) if no revision could be identified from the |
| 1471 supplied options. |
| 1472 """ |
| 1473 # Is there a commit position for this build key? |
| 1474 try: |
| 1475 return GetCommitPosition(options, project=project) |
| 1476 except NoIdentifiedRevision: |
| 1477 pass |
| 1478 |
| 1479 # Nope; derive the sort key from the 'got_[*_]revision' build properties. Note |
| 1480 # that this could be a Git commit (post flag day). |
| 1481 if project: |
| 1482 revision_key = 'got_%s_revision' % (project,) |
| 1483 else: |
| 1484 revision_key = 'got_revision' |
| 1485 revision = options.build_properties.get(revision_key) |
| 1486 if revision and not IsGitCommit(revision): |
| 1487 return None, int(revision) |
| 1488 raise NoIdentifiedRevision("Unable to identify revision for revision key " |
| 1489 "[%s]" % (revision_key,)) |
| 1490 |
| 1491 |
| 1492 def GetGitCommit(options, project=None): |
| 1493 """Returns the 'git' commit hash for the specified repository |
| 1494 |
| 1495 This function uses environmental options to identify the 'git' commit hash |
| 1496 for the specified repository. |
| 1497 |
| 1498 Args: |
| 1499 options: Command-line options structure |
| 1500 project: (str/None) The project key to use. If None, use the topmost |
| 1501 repository identification properties. |
| 1502 Raises: (NoIdentifiedRevision) if no git commit could be identified from the |
| 1503 supplied options. |
| 1504 """ |
| 1505 if project: |
| 1506 git_commit_key = 'got_%s_revision_git' % (project,) |
| 1507 else: |
| 1508 git_commit_key = 'got_revision_git' |
| 1509 commit = options.build_properties.get(git_commit_key) |
| 1510 if commit: |
| 1511 return commit |
| 1512 |
| 1513 # Is 'got_[_*]revision' itself is the Git commit? |
| 1514 if project: |
| 1515 commit_key = 'got_%s_revision' % (project,) |
| 1516 else: |
| 1517 commit_key = 'got_revision' |
| 1518 commit = options.build_properties.get(commit_key) |
| 1519 if commit and IsGitCommit(commit): |
| 1520 return commit |
| 1521 raise NoIdentifiedRevision("Unable to identify commit for commit key: %s" % ( |
| 1522 (git_commit_key, commit_key),)) |
| 1523 |
| 1524 |
| 1525 def GetSortableUploadPathForSortKey(branch, value, delimiter=None): |
| 1526 """Returns: (str) the canonical sort key path constructed from a sort key. |
| 1527 |
| 1528 Returns a canonical sort key path for a sort key. The result will be one of |
| 1529 the following forms: |
| 1530 - (Without Branch or With Branch=='refs/heads/master'): <value> (e.g., 12345) |
| 1531 - (With non-Master Branch): <branch-path>-<value> (e.g., |
| 1532 "refs_my-branch-12345") |
| 1533 |
| 1534 When a 'branch' is supplied, it is converted to a path-suitable form. This |
| 1535 conversion replaces undesirable characters ('/') with underscores. |
| 1536 |
| 1537 Note that when parsing the upload path, 'rsplit' should be used to isolate the |
| 1538 commit position value, as the branch path may have instances of the delimiter |
| 1539 in it. |
| 1540 |
| 1541 See 'GetBuildSortKey' for more information about sort keys. |
| 1542 |
| 1543 Args: |
| 1544 branch: (str/None) The sort key branch, or 'None' if there is no associated |
| 1545 branch. |
| 1546 value: (int) The sort key value. |
| 1547 delimiter: (str) The delimiter to insert in between <branch-path> and |
| 1548 <value> when constructing the branch-inclusive form. If omitted |
| 1549 (default), a hyphen ('-') will be used. |
| 1550 """ |
| 1551 if branch and branch != 'refs/heads/master': |
| 1552 delimiter = delimiter or '-' |
| 1553 branch = branch.replace('/', '_') |
| 1554 return '%s%s%s' % (branch, delimiter, value) |
| 1555 return str(value) |
| 1556 |
| 1557 |
| 1558 def ParseCommitPosition(value): |
| 1559 """Returns: The (branch, value) parsed from a commit position string. |
| 1560 |
| 1561 Args: |
| 1562 value: (str) The value to parse. |
| 1563 Raises: |
| 1564 ValueError: If a commit position could not be parsed from 'value'. |
| 1565 """ |
| 1566 match = COMMIT_POSITION_RE.match(value) |
| 1567 if not match: |
| 1568 raise ValueError("Failed to parse commit position from '%s'" % (value,)) |
| 1569 return match.group(1), int(match.group(2)) |
| 1570 |
| 1571 |
| 1572 def BuildCommitPosition(branch, value): |
| 1573 """Returns: A constructed commit position. |
| 1574 |
| 1575 An example commit position for branch 'refs/heads/master' value '12345' is: |
| 1576 refs/heads/master@{#12345} |
| 1577 |
| 1578 This value can be parsed via 'ParseCommitPosition'. |
| 1579 |
| 1580 Args: |
| 1581 branch: (str) The name of the commit position branch |
| 1582 value: (int): The commit position number. |
| 1583 """ |
| 1584 return '%s@{#%s}' % (branch, value) |
| 1585 |
| 1586 |
| 1587 def GetCommitPosition(options, project=None): |
| 1588 """Returns: (branch, value) The parsed commit position from build options. |
| 1589 |
| 1590 Returns the parsed commit position from the build options. This is identified |
| 1591 by examining the 'got_revision_cp' (or 'got_REPO_revision_cp', if 'project' is |
| 1592 specified) keys. |
| 1593 |
| 1594 Args: |
| 1595 options: Command-line options structure |
| 1596 project: (str/None) If not None, the project to get the build sort key |
| 1597 for. Otherwise, the build-wide sort key will be used. |
| 1598 Returns: (branch, value) The qualified commit position value |
| 1599 Raises: |
| 1600 NoIdentifiedRevision: if no revision could be identified from the |
| 1601 supplied options. |
| 1602 ValueError: If the supplied commit position failed to parse successfully. |
| 1603 """ |
| 1604 if project: |
| 1605 key = 'got_%s_revision_cp' % (project,) |
| 1606 else: |
| 1607 key = 'got_revision_cp' |
| 1608 cp = options.build_properties.get(key) |
| 1609 if not cp: |
| 1610 raise NoIdentifiedRevision("Unable to identify the commit position; the " |
| 1611 "build property is missing: %s" % (key,)) |
| 1612 return ParseCommitPosition(cp) |
| 1613 |
| 1614 |
| 1615 def AddPropertiesOptions(option_parser): |
| 1616 """Registers command line options for parsing build and factory properties. |
| 1617 |
| 1618 After parsing, the options object will have the 'build_properties' and |
| 1619 'factory_properties' attributes. The corresponding values will be python |
| 1620 dictionaries containing the properties. If the options are not given on |
| 1621 the command line, the dictionaries will be empty. |
| 1622 |
| 1623 Args: |
| 1624 option_parser: An optparse.OptionParser to register command line options |
| 1625 for build and factory properties. |
| 1626 """ |
| 1627 option_parser.add_option('--build-properties', action='callback', |
| 1628 callback=convert_json, type='string', |
| 1629 nargs=1, default={}, |
| 1630 help='build properties in JSON format') |
| 1631 option_parser.add_option('--factory-properties', action='callback', |
| 1632 callback=convert_json, type='string', |
| 1633 nargs=1, default={}, |
| 1634 help='factory properties in JSON format') |
| 1635 |
| 1636 |
| 1637 def AddThirdPartyLibToPath(lib, override=False): |
| 1638 """Adds the specified dir in build/third_party to sys.path. |
| 1639 |
| 1640 Setting 'override' to true will place the directory in the beginning of |
| 1641 sys.path, useful for overriding previously set packages. |
| 1642 |
| 1643 NOTE: We would like to deprecate this method, as it allows (encourages?) |
| 1644 scripts to define their own one-off Python path sequences, creating a |
| 1645 difficult-to-manage state where different scripts and libraries have |
| 1646 different path expectations. Please don't use this method if possible; |
| 1647 it preferred to augment 'common.env' instead. |
| 1648 """ |
| 1649 libpath = os.path.abspath(os.path.join(BUILD_DIR, 'third_party', lib)) |
| 1650 if override: |
| 1651 sys.path.insert(0, libpath) |
| 1652 else: |
| 1653 sys.path.append(libpath) |
| 1654 |
| 1655 |
| 1656 def GetLKGR(): |
| 1657 """Connect to chromium LKGR server and get LKGR revision. |
| 1658 |
| 1659 On success, returns the LKGR and 'ok'. On error, returns None and the text of |
| 1660 the error message. |
| 1661 """ |
| 1662 |
| 1663 try: |
| 1664 conn = urllib.urlopen('https://chromium-status.appspot.com/lkgr') |
| 1665 except IOError: |
| 1666 return (None, 'Error connecting to LKGR server! Is your internet ' |
| 1667 'connection working properly?') |
| 1668 try: |
| 1669 rev = int('\n'.join(conn.readlines())) |
| 1670 except IOError: |
| 1671 return (None, 'Error connecting to LKGR server! Is your internet ' |
| 1672 'connection working properly?') |
| 1673 except ValueError: |
| 1674 return None, 'LKGR server returned malformed data! Aborting...' |
| 1675 finally: |
| 1676 conn.close() |
| 1677 |
| 1678 return rev, 'ok' |
| 1679 |
| 1680 |
| 1681 def AbsoluteCanonicalPath(*path): |
| 1682 """Return the most canonical path Python can provide.""" |
| 1683 |
| 1684 file_path = os.path.join(*path) |
| 1685 return os.path.realpath(os.path.abspath(os.path.expanduser(file_path))) |
| 1686 |
| 1687 |
| 1688 def IsolatedImportFromPath(path, extra_paths=None): |
| 1689 dir_path, module_file = os.path.split(path) |
| 1690 module_file = os.path.splitext(module_file)[0] |
| 1691 |
| 1692 saved = sys.path |
| 1693 sys.path = [dir_path] + (extra_paths or []) |
| 1694 try: |
| 1695 return __import__(module_file) |
| 1696 except ImportError: |
| 1697 pass |
| 1698 finally: |
| 1699 sys.path = saved |
| 1700 |
| 1701 |
| 1702 @contextmanager |
| 1703 def MultiPool(processes): |
| 1704 """Manages a multiprocessing.Pool making sure to close the pool when done. |
| 1705 |
| 1706 This will also call pool.terminate() when an exception is raised (and |
| 1707 re-raised the exception to the calling procedure can handle it). |
| 1708 """ |
| 1709 try: |
| 1710 pool = multiprocessing.Pool(processes=processes) |
| 1711 yield pool |
| 1712 pool.close() |
| 1713 except: |
| 1714 pool.terminate() |
| 1715 raise |
| 1716 finally: |
| 1717 pool.join() |
| 1718 |
| 1719 |
| 1720 def ReadJsonAsUtf8(filename=None, text=None): |
| 1721 """Read a json file or string and output a dict. |
| 1722 |
| 1723 This function is different from json.load and json.loads in that it |
| 1724 returns utf8-encoded string for keys and values instead of unicode. |
| 1725 |
| 1726 Args: |
| 1727 filename: path of a file to parse |
| 1728 text: json string to parse |
| 1729 |
| 1730 If both 'filename' and 'text' are provided, 'filename' is used. |
| 1731 """ |
| 1732 def _decode_list(data): |
| 1733 rv = [] |
| 1734 for item in data: |
| 1735 if isinstance(item, unicode): |
| 1736 item = item.encode('utf-8') |
| 1737 elif isinstance(item, list): |
| 1738 item = _decode_list(item) |
| 1739 elif isinstance(item, dict): |
| 1740 item = _decode_dict(item) |
| 1741 rv.append(item) |
| 1742 return rv |
| 1743 |
| 1744 def _decode_dict(data): |
| 1745 rv = {} |
| 1746 for key, value in data.iteritems(): |
| 1747 if isinstance(key, unicode): |
| 1748 key = key.encode('utf-8') |
| 1749 if isinstance(value, unicode): |
| 1750 value = value.encode('utf-8') |
| 1751 elif isinstance(value, list): |
| 1752 value = _decode_list(value) |
| 1753 elif isinstance(value, dict): |
| 1754 value = _decode_dict(value) |
| 1755 rv[key] = value |
| 1756 return rv |
| 1757 |
| 1758 if filename: |
| 1759 with open(filename, 'rb') as f: |
| 1760 return json.load(f, object_hook=_decode_dict) |
| 1761 if text: |
| 1762 return json.loads(text, object_hook=_decode_dict) |
| 1763 |
| 1764 |
| 1765 def GetMasterDevParameters(filename='master_cfg_params.json'): |
| 1766 """Look for master development parameter files in the master directory. |
| 1767 |
| 1768 Return the parsed content if the file exists, as a dictionary. |
| 1769 Every string value in the dictionary is utf8-encoded str. |
| 1770 |
| 1771 If the file is not found, returns an empty dict. This is on purpose, to |
| 1772 make the file optional. |
| 1773 """ |
| 1774 if os.path.isfile(filename): |
| 1775 return ReadJsonAsUtf8(filename=filename) |
| 1776 return {} |
| 1777 |
| 1778 |
| 1779 def FileExclusions(): |
| 1780 all_platforms = ['.landmines', 'obj', 'gen', '.ninja_deps', '.ninja_log'] |
| 1781 # Skip files that the testers don't care about. Mostly directories. |
| 1782 if IsWindows(): |
| 1783 # Remove obj or lib dir entries |
| 1784 return all_platforms + ['cfinstaller_archive', 'lib', 'installer_archive'] |
| 1785 if IsMac(): |
| 1786 return all_platforms + [ |
| 1787 # We don't need the arm bits v8 builds. |
| 1788 'd8_arm', 'v8_shell_arm', |
| 1789 # pdfsqueeze is a build helper, no need to copy it to testers. |
| 1790 'pdfsqueeze', |
| 1791 # We copy the framework into the app bundle, we don't need the second |
| 1792 # copy outside the app. |
| 1793 # TODO(mark): Since r28431, the copy in the build directory is actually |
| 1794 # used by tests. Putting two copies in the .zip isn't great, so maybe |
| 1795 # we can find another workaround. |
| 1796 # 'Chromium Framework.framework', |
| 1797 # 'Google Chrome Framework.framework', |
| 1798 # We copy the Helper into the app bundle, we don't need the second |
| 1799 # copy outside the app. |
| 1800 'Chromium Helper.app', |
| 1801 'Google Chrome Helper.app', |
| 1802 'App Shim Socket', |
| 1803 '.deps', 'obj.host', 'obj.target', 'lib' |
| 1804 ] |
| 1805 if IsLinux(): |
| 1806 return all_platforms + [ |
| 1807 # intermediate build directories (full of .o, .d, etc.). |
| 1808 'appcache', 'glue', 'lib.host', 'obj.host', |
| 1809 'obj.target', 'src', '.deps', |
| 1810 # scons build cruft |
| 1811 '.sconsign.dblite', |
| 1812 # build helper, not needed on testers |
| 1813 'mksnapshot', |
| 1814 ] |
| 1815 |
| 1816 return all_platforms |
| 1817 |
| 1818 |
| 1819 def DatabaseSetup(buildmaster_config, require_dbconfig=False): |
| 1820 """Read database credentials in the master directory.""" |
| 1821 if os.path.isfile('.dbconfig'): |
| 1822 values = {} |
| 1823 execfile('.dbconfig', values) |
| 1824 if 'password' not in values: |
| 1825 raise Exception('could not get db password') |
| 1826 |
| 1827 buildmaster_config['db_url'] = 'postgresql://%s:%s@%s/%s' % ( |
| 1828 values['username'], values['password'], |
| 1829 values.get('hostname', 'localhost'), values['dbname']) |
| 1830 else: |
| 1831 assert not require_dbconfig |
| 1832 |
| 1833 |
| 1834 def ReadBuildersFile(builders_path): |
| 1835 with open(builders_path) as fp: |
| 1836 contents = fp.read() |
| 1837 return ParseBuildersFileContents(builders_path, contents) |
| 1838 |
| 1839 |
| 1840 def ParseBuildersFileContents(path, contents): |
| 1841 builders = ast.literal_eval(contents) |
| 1842 |
| 1843 # Set some additional derived fields that are derived from the |
| 1844 # file's location in the filesystem. |
| 1845 basedir = os.path.dirname(os.path.abspath(path)) |
| 1846 master_dirname = os.path.basename(basedir) |
| 1847 master_name_comps = master_dirname.split('.')[1:] |
| 1848 buildbot_path = '.'.join(master_name_comps) |
| 1849 master_classname = ''.join(c[0].upper() + c[1:] for c in master_name_comps) |
| 1850 builders['master_dirname'] = master_dirname |
| 1851 builders.setdefault('master_classname', master_classname) |
| 1852 builders.setdefault('buildbot_url', |
| 1853 'https://build.chromium.org/p/%s/' % buildbot_path) |
| 1854 |
| 1855 builders.setdefault('buildbucket_bucket', None) |
| 1856 builders.setdefault('service_account_file', None) |
| 1857 |
| 1858 # The _str fields are printable representations of Python values: |
| 1859 # if builders['foo'] == "hello", then builders['foo_str'] == "'hello'". |
| 1860 # This allows them to be read back in by Python scripts properly. |
| 1861 builders['buildbucket_bucket_str'] = repr(builders['buildbucket_bucket']) |
| 1862 builders['service_account_file_str'] = repr(builders['service_account_file']) |
| 1863 |
| 1864 return builders |
| 1865 |
| 1866 |
| 1867 def GetSlavesFromBuildersFile(builders_path): |
| 1868 """Read builders_path and return a list of slave dicts.""" |
| 1869 builders = ReadBuildersFile(builders_path) |
| 1870 return GetSlavesFromBuilders(builders) |
| 1871 |
| 1872 |
| 1873 def GetSlavesFromBuilders(builders): |
| 1874 """Returns a list of slave dicts derived from the builders dict.""" |
| 1875 builders_in_pool = {} |
| 1876 |
| 1877 # builders.pyl contains a list of builders -> slave_pools |
| 1878 # and a list of slave_pools -> slaves. |
| 1879 # We require that each slave is in a single pool, but each slave |
| 1880 # may have multiple builders, so we need to build up the list of |
| 1881 # builders each slave pool supports. |
| 1882 for builder_name, builder_vals in builders['builders'].items(): |
| 1883 pool_names = builder_vals['slave_pools'] |
| 1884 for pool_name in pool_names: |
| 1885 if pool_name not in builders_in_pool: |
| 1886 builders_in_pool[pool_name] = set() |
| 1887 pool_data = builders['slave_pools'][pool_name] |
| 1888 for slave in pool_data['slaves']: |
| 1889 builders_in_pool[pool_name].add(builder_name) |
| 1890 |
| 1891 # Now we can generate the list of slaves using the above lookup table. |
| 1892 slaves = [] |
| 1893 for pool_name, pool_data in builders['slave_pools'].items(): |
| 1894 slave_data = pool_data['slave_data'] |
| 1895 builder_names = sorted(builders_in_pool[pool_name]) |
| 1896 for slave in pool_data['slaves']: |
| 1897 slaves.append({ |
| 1898 'hostname': slave, |
| 1899 'builder_name': builder_names, |
| 1900 'master': builders['master_classname'], |
| 1901 'os': slave_data['os'], |
| 1902 'version': slave_data['version'], |
| 1903 'bits': slave_data['bits'], |
| 1904 }) |
| 1905 |
| 1906 return slaves |
| 1907 |
| 1908 def GetSlaveNamesForBuilder(builders, builder_name): |
| 1909 """Returns a list of slave hostnames for the given builder name.""" |
| 1910 slaves = [] |
| 1911 pool_names = builders['builders'][builder_name]['slave_pools'] |
| 1912 for pool_name in pool_names: |
| 1913 slaves.extend(builders['slave_pools'][pool_name]['slaves']) |
| 1914 return slaves |
OLD | NEW |