• 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  if not zipfile.is_zipfile(zip_path):
229    raise Exception('Invalid zip file: %s' % zip_path)
230
231  with zipfile.ZipFile(zip_path) as z:
232    for name in z.namelist():
233      if name.endswith('/'):
234        continue
235      if pattern is not None:
236        if not fnmatch.fnmatch(name, pattern):
237          continue
238      if predicate and not predicate(name):
239        continue
240      CheckZipPath(name)
241      if no_clobber:
242        output_path = os.path.join(path, name)
243        if os.path.exists(output_path):
244          raise Exception(
245              'Path already exists from zip: %s %s %s'
246              % (zip_path, name, output_path))
247      if IsSymlink(z, name):
248        dest = os.path.join(path, name)
249        MakeDirectory(os.path.dirname(dest))
250        os.symlink(z.read(name), dest)
251      else:
252        z.extract(name, path)
253
254
255def AddToZipHermetic(zip_file, zip_path, src_path=None, data=None,
256                     compress=None):
257  """Adds a file to the given ZipFile with a hard-coded modified time.
258
259  Args:
260    zip_file: ZipFile instance to add the file to.
261    zip_path: Destination path within the zip file.
262    src_path: Path of the source file. Mutually exclusive with |data|.
263    data: File data as a string.
264    compress: Whether to enable compression. Default is take from ZipFile
265        constructor.
266  """
267  assert (src_path is None) != (data is None), (
268      '|src_path| and |data| are mutually exclusive.')
269  CheckZipPath(zip_path)
270  zipinfo = zipfile.ZipInfo(filename=zip_path, date_time=_HERMETIC_TIMESTAMP)
271  zipinfo.external_attr = _HERMETIC_FILE_ATTR
272
273  if src_path and os.path.islink(src_path):
274    zipinfo.filename = zip_path
275    zipinfo.external_attr |= stat.S_IFLNK << 16L # mark as a symlink
276    zip_file.writestr(zipinfo, os.readlink(src_path))
277    return
278
279  if src_path:
280    with file(src_path) as f:
281      data = f.read()
282
283  # zipfile will deflate even when it makes the file bigger. To avoid
284  # growing files, disable compression at an arbitrary cut off point.
285  if len(data) < 16:
286    compress = False
287
288  # None converts to ZIP_STORED, when passed explicitly rather than the
289  # default passed to the ZipFile constructor.
290  compress_type = zip_file.compression
291  if compress is not None:
292    compress_type = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
293  zip_file.writestr(zipinfo, data, compress_type)
294
295
296def DoZip(inputs, output, base_dir=None):
297  """Creates a zip file from a list of files.
298
299  Args:
300    inputs: A list of paths to zip, or a list of (zip_path, fs_path) tuples.
301    output: Destination .zip file.
302    base_dir: Prefix to strip from inputs.
303  """
304  input_tuples = []
305  for tup in inputs:
306    if isinstance(tup, basestring):
307      tup = (os.path.relpath(tup, base_dir), tup)
308    input_tuples.append(tup)
309
310  # Sort by zip path to ensure stable zip ordering.
311  input_tuples.sort(key=lambda tup: tup[0])
312  with zipfile.ZipFile(output, 'w') as outfile:
313    for zip_path, fs_path in input_tuples:
314      AddToZipHermetic(outfile, zip_path, src_path=fs_path)
315
316
317def ZipDir(output, base_dir):
318  """Creates a zip file from a directory."""
319  inputs = []
320  for root, _, files in os.walk(base_dir):
321    for f in files:
322      inputs.append(os.path.join(root, f))
323  DoZip(inputs, output, base_dir)
324
325
326def MatchesGlob(path, filters):
327  """Returns whether the given path matches any of the given glob patterns."""
328  return filters and any(fnmatch.fnmatch(path, f) for f in filters)
329
330
331def MergeZips(output, inputs, exclude_patterns=None, path_transform=None):
332  path_transform = path_transform or (lambda p, z: p)
333  added_names = set()
334
335  with zipfile.ZipFile(output, 'w') as out_zip:
336    for in_file in inputs:
337      with zipfile.ZipFile(in_file, 'r') as in_zip:
338        in_zip._expected_crc = None
339        for info in in_zip.infolist():
340          # Ignore directories.
341          if info.filename[-1] == '/':
342            continue
343          dst_name = path_transform(info.filename, in_file)
344          already_added = dst_name in added_names
345          if not already_added and not MatchesGlob(dst_name, exclude_patterns):
346            AddToZipHermetic(out_zip, dst_name, data=in_zip.read(info))
347            added_names.add(dst_name)
348
349
350def PrintWarning(message):
351  print 'WARNING: ' + message
352
353
354def PrintBigWarning(message):
355  print '*****     ' * 8
356  PrintWarning(message)
357  print '*****     ' * 8
358
359
360def GetSortedTransitiveDependencies(top, deps_func):
361  """Gets the list of all transitive dependencies in sorted order.
362
363  There should be no cycles in the dependency graph.
364
365  Args:
366    top: a list of the top level nodes
367    deps_func: A function that takes a node and returns its direct dependencies.
368  Returns:
369    A list of all transitive dependencies of nodes in top, in order (a node will
370    appear in the list at a higher index than all of its dependencies).
371  """
372  def Node(dep):
373    return (dep, deps_func(dep))
374
375  # First: find all deps
376  unchecked_deps = list(top)
377  all_deps = set(top)
378  while unchecked_deps:
379    dep = unchecked_deps.pop()
380    new_deps = deps_func(dep).difference(all_deps)
381    unchecked_deps.extend(new_deps)
382    all_deps = all_deps.union(new_deps)
383
384  # Then: simple, slow topological sort.
385  sorted_deps = []
386  unsorted_deps = dict(map(Node, all_deps))
387  while unsorted_deps:
388    for library, dependencies in unsorted_deps.items():
389      if not dependencies.intersection(unsorted_deps.keys()):
390        sorted_deps.append(library)
391        del unsorted_deps[library]
392
393  return sorted_deps
394
395
396def GetPythonDependencies():
397  """Gets the paths of imported non-system python modules.
398
399  A path is assumed to be a "system" import if it is outside of chromium's
400  src/. The paths will be relative to the current directory.
401  """
402  module_paths = (m.__file__ for m in sys.modules.itervalues()
403                  if m is not None and hasattr(m, '__file__'))
404
405  abs_module_paths = map(os.path.abspath, module_paths)
406
407  assert os.path.isabs(host_paths.DIR_SOURCE_ROOT)
408  non_system_module_paths = [
409      p for p in abs_module_paths if p.startswith(host_paths.DIR_SOURCE_ROOT)]
410  def ConvertPycToPy(s):
411    if s.endswith('.pyc'):
412      return s[:-1]
413    return s
414
415  non_system_module_paths = map(ConvertPycToPy, non_system_module_paths)
416  non_system_module_paths = map(os.path.relpath, non_system_module_paths)
417  return sorted(set(non_system_module_paths))
418
419
420def AddDepfileOption(parser):
421  # TODO(agrieve): Get rid of this once we've moved to argparse.
422  if hasattr(parser, 'add_option'):
423    func = parser.add_option
424  else:
425    func = parser.add_argument
426  func('--depfile',
427       help='Path to depfile. Must be specified as the action\'s first output.')
428
429
430def WriteDepfile(path, dependencies):
431  with open(path, 'w') as depfile:
432    depfile.write(path)
433    depfile.write(': ')
434    depfile.write(' '.join(dependencies))
435    depfile.write('\n')
436
437
438def ExpandFileArgs(args):
439  """Replaces file-arg placeholders in args.
440
441  These placeholders have the form:
442    @FileArg(filename:key1:key2:...:keyn)
443
444  The value of such a placeholder is calculated by reading 'filename' as json.
445  And then extracting the value at [key1][key2]...[keyn].
446
447  Note: This intentionally does not return the list of files that appear in such
448  placeholders. An action that uses file-args *must* know the paths of those
449  files prior to the parsing of the arguments (typically by explicitly listing
450  them in the action's inputs in build files).
451  """
452  new_args = list(args)
453  file_jsons = dict()
454  r = re.compile('@FileArg\((.*?)\)')
455  for i, arg in enumerate(args):
456    match = r.search(arg)
457    if not match:
458      continue
459
460    if match.end() != len(arg):
461      raise Exception('Unexpected characters after FileArg: ' + arg)
462
463    lookup_path = match.group(1).split(':')
464    file_path = lookup_path[0]
465    if not file_path in file_jsons:
466      file_jsons[file_path] = ReadJson(file_path)
467
468    expansion = file_jsons[file_path]
469    for k in lookup_path[1:]:
470      expansion = expansion[k]
471
472    new_args[i] = arg[:match.start()] + str(expansion)
473
474  return new_args
475
476
477def CallAndWriteDepfileIfStale(function, options, record_path=None,
478                               input_paths=None, input_strings=None,
479                               output_paths=None, force=False,
480                               pass_changes=False,
481                               depfile_deps=None):
482  """Wraps md5_check.CallAndRecordIfStale() and also writes dep & stamp files.
483
484  Depfiles and stamp files are automatically added to output_paths when present
485  in the |options| argument. They are then created after |function| is called.
486
487  By default, only python dependencies are added to the depfile. If there are
488  other input paths that are not captured by GN deps, then they should be listed
489  in depfile_deps. It's important to write paths to the depfile that are already
490  captured by GN deps since GN args can cause GN deps to change, and such
491  changes are not immediately reflected in depfiles (http://crbug.com/589311).
492  """
493  if not output_paths:
494    raise Exception('At least one output_path must be specified.')
495  input_paths = list(input_paths or [])
496  input_strings = list(input_strings or [])
497  output_paths = list(output_paths or [])
498
499  python_deps = None
500  if hasattr(options, 'depfile') and options.depfile:
501    python_deps = GetPythonDependencies()
502    input_paths += python_deps
503    output_paths += [options.depfile]
504
505  stamp_file = hasattr(options, 'stamp') and options.stamp
506  if stamp_file:
507    output_paths += [stamp_file]
508
509  def on_stale_md5(changes):
510    args = (changes,) if pass_changes else ()
511    function(*args)
512    if python_deps is not None:
513      all_depfile_deps = list(python_deps)
514      if depfile_deps:
515        all_depfile_deps.extend(depfile_deps)
516      WriteDepfile(options.depfile, all_depfile_deps)
517    if stamp_file:
518      Touch(stamp_file)
519
520  md5_check.CallAndRecordIfStale(
521      on_stale_md5,
522      record_path=record_path,
523      input_paths=input_paths,
524      input_strings=input_strings,
525      output_paths=output_paths,
526      force=force,
527      pass_changes=True)
528
529