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):
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('/'):
206 output_path = os.path.join(path, name)
207 if os.path.exists(output_path):
209 'Path already exists from zip: %s %s %s'
210 % (zip_path, name, output_path))
212 z.extractall(path=path)
215 def DoZip(inputs, output, base_dir):
216 with zipfile.ZipFile(output, 'w') as outfile:
218 CheckZipPath(os.path.relpath(f, base_dir))
219 outfile.write(f, os.path.relpath(f, base_dir))
222 def ZipDir(output, base_dir):
223 with zipfile.ZipFile(output, 'w') as outfile:
224 for root, _, files in os.walk(base_dir):
226 path = os.path.join(root, f)
227 archive_path = os.path.relpath(path, base_dir)
228 CheckZipPath(archive_path)
229 outfile.write(path, archive_path)
232 def PrintWarning(message):
233 print 'WARNING: ' + message
236 def PrintBigWarning(message):
238 PrintWarning(message)
242 def GetSortedTransitiveDependencies(top, deps_func):
243 """Gets the list of all transitive dependencies in sorted order.
245 There should be no cycles in the dependency graph.
248 top: a list of the top level nodes
249 deps_func: A function that takes a node and returns its direct dependencies.
251 A list of all transitive dependencies of nodes in top, in order (a node will
252 appear in the list at a higher index than all of its dependencies).
255 return (dep, deps_func(dep))
257 # First: find all deps
258 unchecked_deps = list(top)
260 while unchecked_deps:
261 dep = unchecked_deps.pop()
262 new_deps = deps_func(dep).difference(all_deps)
263 unchecked_deps.extend(new_deps)
264 all_deps = all_deps.union(new_deps)
266 # Then: simple, slow topological sort.
268 unsorted_deps = dict(map(Node, all_deps))
270 for library, dependencies in unsorted_deps.items():
271 if not dependencies.intersection(unsorted_deps.keys()):
272 sorted_deps.append(library)
273 del unsorted_deps[library]
278 def GetPythonDependencies():
279 """Gets the paths of imported non-system python modules.
281 A path is assumed to be a "system" import if it is outside of chromium's
282 src/. The paths will be relative to the current directory.
284 module_paths = (m.__file__ for m in sys.modules.itervalues()
285 if m is not None and hasattr(m, '__file__'))
287 abs_module_paths = map(os.path.abspath, module_paths)
289 non_system_module_paths = [
290 p for p in abs_module_paths if p.startswith(CHROMIUM_SRC)]
291 def ConvertPycToPy(s):
292 if s.endswith('.pyc'):
296 non_system_module_paths = map(ConvertPycToPy, non_system_module_paths)
297 non_system_module_paths = map(os.path.relpath, non_system_module_paths)
298 return sorted(set(non_system_module_paths))
301 def AddDepfileOption(parser):
302 parser.add_option('--depfile',
303 help='Path to depfile. This must be specified as the '
304 'action\'s first output.')
307 def WriteDepfile(path, dependencies):
308 with open(path, 'w') as depfile:
311 depfile.write(' '.join(dependencies))
315 def ExpandFileArgs(args):
316 """Replaces file-arg placeholders in args.
318 These placeholders have the form:
319 @FileArg(filename:key1:key2:...:keyn)
321 The value of such a placeholder is calculated by reading 'filename' as json.
322 And then extracting the value at [key1][key2]...[keyn].
324 Note: This intentionally does not return the list of files that appear in such
325 placeholders. An action that uses file-args *must* know the paths of those
326 files prior to the parsing of the arguments (typically by explicitly listing
327 them in the action's inputs in build files).
329 new_args = list(args)
331 r = re.compile('@FileArg\((.*?)\)')
332 for i, arg in enumerate(args):
333 match = r.search(arg)
337 if match.end() != len(arg):
338 raise Exception('Unexpected characters after FileArg: ' + arg)
340 lookup_path = match.group(1).split(':')
341 file_path = lookup_path[0]
342 if not file_path in file_jsons:
343 file_jsons[file_path] = ReadJson(file_path)
345 expansion = file_jsons[file_path]
346 for k in lookup_path[1:]:
347 expansion = expansion[k]
349 new_args[i] = arg[:match.start()] + str(expansion)