Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(40)

Side by Side Diff: infra/scripts/legacy/scripts/common/chromium_utils.py

Issue 1213433006: Fork runtest.py and everything it needs src-side for easier hacking (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: runisolatedtest.py Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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
OLDNEW
« no previous file with comments | « infra/scripts/legacy/scripts/common/__init__.py ('k') | infra/scripts/legacy/scripts/common/env.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698