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.
20 CHROMIUM_SRC = os.path.normpath(
21 os.path.join(os.path.dirname(__file__),
22 os.pardir, os.pardir, os.pardir, os.pardir))
23 COLORAMA_ROOT = os.path.join(CHROMIUM_SRC,
24 'third_party', 'colorama', 'src')
27 @contextlib.contextmanager
29 dirname = tempfile.mkdtemp()
33 shutil.rmtree(dirname)
36 def MakeDirectory(dir_path):
43 def DeleteDirectory(dir_path):
44 if os.path.exists(dir_path):
45 shutil.rmtree(dir_path)
48 def Touch(path, fail_if_missing=False):
49 if fail_if_missing and not os.path.exists(path):
50 raise Exception(path + ' doesn\'t exist.')
52 MakeDirectory(os.path.dirname(path))
57 def FindInDirectory(directory, filename_filter):
59 for root, _dirnames, filenames in os.walk(directory):
60 matched_files = fnmatch.filter(filenames, filename_filter)
61 files.extend((os.path.join(root, f) for f in matched_files))
65 def FindInDirectories(directories, filename_filter):
67 for directory in directories:
68 all_files.extend(FindInDirectory(directory, filename_filter))
72 def ParseGnList(gn_string):
73 return ast.literal_eval(gn_string)
76 def ParseGypList(gyp_string):
77 # The ninja generator doesn't support $ in strings, so use ## to
79 # TODO(cjhopman): Remove when
80 # https://code.google.com/p/gyp/issues/detail?id=327
82 gyp_string = gyp_string.replace('##', '$')
84 if gyp_string.startswith('['):
85 return ParseGnList(gyp_string)
86 return shlex.split(gyp_string)
89 def CheckOptions(options, parser, required=None):
92 for option_name in required:
93 if getattr(options, option_name) is None:
94 parser.error('--%s is required' % option_name.replace('_', '-'))
97 def WriteJson(obj, path, only_if_changed=False):
99 if os.path.exists(path):
100 with open(path, 'r') as oldfile:
101 old_dump = oldfile.read()
103 new_dump = json.dumps(obj, sort_keys=True, indent=2, separators=(',', ': '))
105 if not only_if_changed or old_dump != new_dump:
106 with open(path, 'w') as outfile:
107 outfile.write(new_dump)
111 with open(path, 'r') as jsonfile:
112 return json.load(jsonfile)
115 class CalledProcessError(Exception):
116 """This exception is raised when the process run by CheckOutput
117 exits with a non-zero exit code."""
119 def __init__(self, cwd, args, output):
120 super(CalledProcessError, self).__init__()
126 # A user should be able to simply copy and paste the command that failed
128 copyable_command = '( cd {}; {} )'.format(os.path.abspath(self.cwd),
129 ' '.join(map(pipes.quote, self.args)))
130 return 'Command failed: {}\n{}'.format(copyable_command, self.output)
133 # This can be used in most cases like subprocess.check_output(). The output,
134 # particularly when the command fails, better highlights the command's failure.
135 # If the command fails, raises a build_utils.CalledProcessError.
136 def CheckOutput(args, cwd=None,
137 print_stdout=False, print_stderr=True,
140 fail_func=lambda returncode, stderr: returncode != 0):
144 child = subprocess.Popen(args,
145 stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
146 stdout, stderr = child.communicate()
148 if stdout_filter is not None:
149 stdout = stdout_filter(stdout)
151 if stderr_filter is not None:
152 stderr = stderr_filter(stderr)
154 if fail_func(child.returncode, stderr):
155 raise CalledProcessError(cwd, args, stdout + stderr)
158 sys.stdout.write(stdout)
160 sys.stderr.write(stderr)
165 def GetModifiedTime(path):
166 # For a symlink, the modified time should be the greater of the link's
167 # modified time and the modified time of the target.
168 return max(os.lstat(path).st_mtime, os.stat(path).st_mtime)
171 def IsTimeStale(output, inputs):
172 if not os.path.exists(output):
175 output_time = GetModifiedTime(output)
177 if GetModifiedTime(i) > output_time:
183 device_state = CheckOutput(['adb', 'get-state'])
184 return device_state.strip() == 'device'
187 def CheckZipPath(name):
188 if os.path.normpath(name) != name:
189 raise Exception('Non-canonical zip path: %s' % name)
190 if os.path.isabs(name):
191 raise Exception('Absolute zip path: %s' % name)
194 def ExtractAll(zip_path, path=None, no_clobber=True, pattern=None):
197 elif not os.path.exists(path):
200 with zipfile.ZipFile(zip_path) as z:
201 for name in z.namelist():
202 if name.endswith('/'):
204 if pattern is not None:
205 if not fnmatch.fnmatch(name, pattern):
209 output_path = os.path.join(path, name)
210 if os.path.exists(output_path):
212 'Path already exists from zip: %s %s %s'
213 % (zip_path, name, output_path))
215 z.extractall(path=path)
218 def DoZip(inputs, output, base_dir):
219 with zipfile.ZipFile(output, 'w') as outfile:
221 CheckZipPath(os.path.relpath(f, base_dir))
222 outfile.write(f, os.path.relpath(f, base_dir))
225 def ZipDir(output, base_dir):
226 with zipfile.ZipFile(output, 'w') as outfile:
227 for root, _, files in os.walk(base_dir):
229 path = os.path.join(root, f)
230 archive_path = os.path.relpath(path, base_dir)
231 CheckZipPath(archive_path)
232 outfile.write(path, archive_path)
235 def MergeZips(output, inputs, exclude_patterns=None):
237 if exclude_patterns is not None:
238 for p in exclude_patterns:
239 if fnmatch.fnmatch(name, p):
243 with zipfile.ZipFile(output, 'w') as out_zip:
244 for in_file in inputs:
245 with zipfile.ZipFile(in_file, 'r') as in_zip:
246 for name in in_zip.namelist():
248 out_zip.writestr(name, in_zip.read(name))
251 def PrintWarning(message):
252 print 'WARNING: ' + message
255 def PrintBigWarning(message):
257 PrintWarning(message)
261 def GetSortedTransitiveDependencies(top, deps_func):
262 """Gets the list of all transitive dependencies in sorted order.
264 There should be no cycles in the dependency graph.
267 top: a list of the top level nodes
268 deps_func: A function that takes a node and returns its direct dependencies.
270 A list of all transitive dependencies of nodes in top, in order (a node will
271 appear in the list at a higher index than all of its dependencies).
274 return (dep, deps_func(dep))
276 # First: find all deps
277 unchecked_deps = list(top)
279 while unchecked_deps:
280 dep = unchecked_deps.pop()
281 new_deps = deps_func(dep).difference(all_deps)
282 unchecked_deps.extend(new_deps)
283 all_deps = all_deps.union(new_deps)
285 # Then: simple, slow topological sort.
287 unsorted_deps = dict(map(Node, all_deps))
289 for library, dependencies in unsorted_deps.items():
290 if not dependencies.intersection(unsorted_deps.keys()):
291 sorted_deps.append(library)
292 del unsorted_deps[library]
297 def GetPythonDependencies():
298 """Gets the paths of imported non-system python modules.
300 A path is assumed to be a "system" import if it is outside of chromium's
301 src/. The paths will be relative to the current directory.
303 module_paths = (m.__file__ for m in sys.modules.itervalues()
304 if m is not None and hasattr(m, '__file__'))
306 abs_module_paths = map(os.path.abspath, module_paths)
308 non_system_module_paths = [
309 p for p in abs_module_paths if p.startswith(CHROMIUM_SRC)]
310 def ConvertPycToPy(s):
311 if s.endswith('.pyc'):
315 non_system_module_paths = map(ConvertPycToPy, non_system_module_paths)
316 non_system_module_paths = map(os.path.relpath, non_system_module_paths)
317 return sorted(set(non_system_module_paths))
320 def AddDepfileOption(parser):
321 parser.add_option('--depfile',
322 help='Path to depfile. This must be specified as the '
323 'action\'s first output.')
326 def WriteDepfile(path, dependencies):
327 with open(path, 'w') as depfile:
330 depfile.write(' '.join(dependencies))
334 def ExpandFileArgs(args):
335 """Replaces file-arg placeholders in args.
337 These placeholders have the form:
338 @FileArg(filename:key1:key2:...:keyn)
340 The value of such a placeholder is calculated by reading 'filename' as json.
341 And then extracting the value at [key1][key2]...[keyn].
343 Note: This intentionally does not return the list of files that appear in such
344 placeholders. An action that uses file-args *must* know the paths of those
345 files prior to the parsing of the arguments (typically by explicitly listing
346 them in the action's inputs in build files).
348 new_args = list(args)
350 r = re.compile('@FileArg\((.*?)\)')
351 for i, arg in enumerate(args):
352 match = r.search(arg)
356 if match.end() != len(arg):
357 raise Exception('Unexpected characters after FileArg: ' + arg)
359 lookup_path = match.group(1).split(':')
360 file_path = lookup_path[0]
361 if not file_path in file_jsons:
362 file_jsons[file_path] = ReadJson(file_path)
364 expansion = file_jsons[file_path]
365 for k in lookup_path[1:]:
366 expansion = expansion[k]
368 new_args[i] = arg[:match.start()] + str(expansion)