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