Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env python | |
| 2 | |
| 3 # Copyright 2014 The Chromium Authors. All rights reserved. | |
| 4 # Use of this source code is governed by a BSD-style license that can be | |
| 5 # found in the LICENSE file. | |
| 6 | |
| 7 '''Produces various output formats from a set of JavaScript files with | |
| 8 closure style require/provide calls. | |
| 9 | |
| 10 Scans one or more directory trees for JavaScript files. Then, from a | |
| 11 given list of top-level files, sorts all required input files topologically. | |
| 12 The top-level files are appended to the sorted list in the order specified | |
| 13 on the command line. If no root directories are specified, the source | |
| 14 files are assumed to be ordered already and no dependency analysis is | |
| 15 performed. The resulting file list can then be output in various ways: | |
| 16 | |
| 17 - list: a plain list of files, one per line. | |
| 18 | |
| 19 - html: html <script> tags with src attributes containing paths. | |
| 20 | |
| 21 - bundle: a cocatenation of all the files, separated by newlines. | |
|
dmazzoni
2014/05/21 17:42:17
cocatenation -> concatenation
| |
| 22 | |
| 23 - compressed_bundle: A bundle where non-significant whitespace, including | |
| 24 comments, has been stripped. | |
| 25 ''' | |
| 26 | |
| 27 | |
| 28 import optparse | |
| 29 import os | |
| 30 import shutil | |
| 31 import sys | |
| 32 | |
| 33 _SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__)) | |
| 34 _CHROME_SOURCE = os.path.realpath( | |
| 35 os.path.join(_SCRIPT_DIR, *[os.path.pardir] * 6)) | |
| 36 sys.path.insert(0, os.path.join( | |
| 37 _CHROME_SOURCE, 'third_party/WebKit/Source/build/scripts')) | |
| 38 sys.path.insert(0, os.path.join( | |
| 39 _CHROME_SOURCE, ('chrome/third_party/chromevox/third_party/' + | |
| 40 'closure-library/closure/bin/build'))) | |
| 41 import depstree | |
| 42 import rjsmin | |
| 43 import source | |
| 44 import treescan | |
| 45 | |
| 46 | |
| 47 def Die(message): | |
| 48 '''Prints an error message and exit the program.''' | |
| 49 print >>sys.stderr, message | |
| 50 sys.exit(1) | |
| 51 | |
| 52 | |
| 53 class SourceWithPaths(source.Source): | |
| 54 '''A source.Source object with its relative input and output paths''' | |
| 55 | |
| 56 def __init__(self, content, in_path, out_path): | |
| 57 super(SourceWithPaths, self).__init__(content) | |
| 58 self._in_path = in_path | |
| 59 self._out_path = out_path | |
| 60 | |
| 61 def GetInPath(self): | |
| 62 return self._in_path | |
| 63 | |
| 64 def GetOutPath(self): | |
| 65 return self._out_path | |
| 66 | |
| 67 | |
| 68 class Bundle(): | |
| 69 '''An ordered list of sources without duplicates.''' | |
| 70 | |
| 71 def __init__(self): | |
| 72 self._added_paths = set() | |
| 73 self._added_sources = [] | |
| 74 | |
| 75 def Add(self, sources): | |
| 76 '''Appends one or more source objects the list if it doesn't already | |
| 77 exist. | |
| 78 | |
| 79 Args: | |
| 80 sources: A SourceWithPath or an iterable of such objects. | |
| 81 ''' | |
| 82 if isinstance(sources, SourceWithPaths): | |
| 83 sources = [sources] | |
| 84 for source in sources: | |
| 85 path = source.GetInPath() | |
| 86 if path not in self._added_paths: | |
| 87 self._added_paths.add(path) | |
| 88 self._added_sources.append(source) | |
| 89 | |
| 90 def GetOutPaths(self): | |
| 91 return (source.GetOutPath() for source in self._added_sources) | |
| 92 | |
| 93 def GetSources(self): | |
| 94 return self._added_sources | |
| 95 | |
| 96 def GetUncompressedSource(self): | |
| 97 return '\n'.join((s.GetSource() for s in self._added_sources)) | |
| 98 | |
| 99 def GetCompressedSource(self): | |
| 100 return rjsmin.jsmin(self.GetUncompressedSource()) | |
| 101 | |
| 102 | |
| 103 class PathRewriter(): | |
| 104 '''A list of simple path rewrite rules to map relative input paths to | |
| 105 relative output paths. | |
| 106 ''' | |
| 107 | |
| 108 def __init__(self, specs): | |
| 109 '''Args: | |
| 110 specs: A list of mappings, each consisting of the input prefix and | |
| 111 the corresponding output prefix separated by colons. | |
| 112 ''' | |
| 113 self._prefix_map = [] | |
| 114 for spec in specs: | |
| 115 parts = spec.split(':') | |
| 116 if len(parts) != 2: | |
| 117 Die('Invalid prefix rewrite spec %s' % spec) | |
| 118 if not parts[0].endswith('/'): | |
| 119 parts[0] += '/' | |
| 120 self._prefix_map.append(parts) | |
| 121 | |
| 122 def RewritePath(self, in_path): | |
| 123 '''Rewrites an input path according to the list of rules. | |
| 124 | |
| 125 Args: | |
| 126 in_path, str: The input path to rewrite. | |
| 127 Returns: | |
| 128 str: The corresponding output path. | |
| 129 ''' | |
| 130 for in_prefix, out_prefix in self._prefix_map: | |
| 131 if in_path.startswith(in_prefix): | |
| 132 return os.path.join(out_prefix, in_path[len(in_prefix):]) | |
| 133 return in_path | |
| 134 | |
| 135 | |
| 136 def ReadSources(options, args): | |
| 137 '''Reads all source specified on the command line, including sources | |
| 138 included by --root options. | |
| 139 ''' | |
| 140 | |
| 141 def EnsureSourceLoaded(in_path, sources, path_rewriter): | |
| 142 if in_path not in sources: | |
| 143 out_path = path_rewriter.RewritePath(in_path) | |
| 144 sources[in_path] = SourceWithPaths(source.GetFileContents(in_path), | |
| 145 in_path, out_path) | |
| 146 | |
| 147 # Only read the actual source file if we will do a dependency analysis or | |
| 148 # if we'll need it for the output. | |
| 149 need_source_text = (len(options.roots) > 0 or | |
| 150 options.mode in ('bundle', 'compressed_bundle')) | |
| 151 path_rewriter = PathRewriter(options.prefix_map) | |
| 152 sources = {} | |
| 153 for root in options.roots: | |
| 154 for name in treescan.ScanTreeForJsFiles(root): | |
| 155 EnsureSourceLoaded(name, sources, path_rewriter) | |
| 156 for path in args: | |
| 157 if need_source_text: | |
| 158 EnsureSourceLoaded(path, sources, path_rewriter) | |
| 159 else: | |
| 160 # Just add an empty representation of the source. | |
| 161 sources[path] = SourceWithPaths( | |
| 162 '', path, path_rewriter.RewritePath(path)) | |
| 163 return sources | |
| 164 | |
| 165 | |
| 166 def CalcDeps(bundle, sources, top_level): | |
| 167 '''Calculates dependencies for a set of top-level files. | |
| 168 | |
| 169 Args: | |
| 170 bundle: Bundle to add the sources to. | |
| 171 sources, dict: Mapping from input path to SourceWithPaths objects. | |
| 172 top_level, list: List of top-level input paths to calculate dependencies | |
| 173 for. | |
| 174 ''' | |
| 175 def GetBase(sources): | |
| 176 for source in sources.itervalues(): | |
| 177 if (os.path.basename(source.GetInPath()) == 'base.js' and | |
| 178 'goog' in source.provides): | |
| 179 return source | |
| 180 Die('goog.base not provided by any file') | |
| 181 | |
| 182 providers = [s for s in sources.itervalues() if len(s.provides) > 0] | |
| 183 deps = depstree.DepsTree(providers) | |
| 184 namespaces = [] | |
| 185 for path in top_level: | |
| 186 namespaces.extend(sources[path].requires) | |
| 187 # base.js is an implicit dependency that always goes first. | |
| 188 bundle.Add(GetBase(sources)) | |
| 189 bundle.Add(deps.GetDependencies(namespaces)) | |
| 190 | |
| 191 | |
| 192 def CopyFiles(sources, dest_dir): | |
| 193 '''Copies a list of sources to a destination directory.''' | |
|
dmazzoni
2014/05/21 17:42:17
I think this function should be called HardLinkOrC
| |
| 194 | |
| 195 def CopyOrLink(src, dst): | |
| 196 if not os.path.exists(os.path.dirname(dst)): | |
| 197 os.makedirs(os.path.dirname(dst)) | |
| 198 if os.path.exists(dst): | |
| 199 # Avoid clobbering the inode if source and destination refer to the | |
| 200 # same file already. | |
| 201 if os.path.samefile(src, dst): | |
| 202 return | |
| 203 os.unlink(dst) | |
| 204 try: | |
| 205 os.link(src, dst) | |
| 206 except: | |
| 207 shutil.copy(src, dst) | |
| 208 | |
| 209 for source in sources: | |
| 210 CopyOrLink(source.GetInPath(), | |
| 211 os.path.join(dest_dir, source.GetOutPath())) | |
| 212 | |
| 213 | |
| 214 def WriteOutput(bundle, format, out_file, dest_dir): | |
| 215 '''Writes output in the specified format. | |
| 216 | |
| 217 Args: | |
| 218 bundle: The ordered bundle iwth all sources already added. | |
| 219 format: Output format, one of list, html, bundle, compressed_bundle. | |
| 220 out_file: File object to receive the output. | |
| 221 dest_dir: Prepended to each path mentioned in the output, if aplicable. | |
|
dmazzoni
2014/05/21 17:42:17
aplicable -> applicable
| |
| 222 ''' | |
| 223 if format == 'list': | |
| 224 paths = bundle.GetOutPaths() | |
| 225 if dest_dir: | |
| 226 paths = (os.path.join(dest_dir, p) for p in paths) | |
| 227 paths = (os.path.normpath(p) for p in paths) | |
| 228 out_file.write('\n'.join(paths)) | |
| 229 elif format == 'html': | |
| 230 HTML_TEMPLATE = '<script src=\'%s\'>' | |
| 231 script_lines = (HTML_TEMPLATE % p for p in bundle.GetOutPaths()) | |
| 232 out_file.write('\n'.join(script_lines)) | |
| 233 elif format == 'bundle': | |
| 234 out_file.write(bundle.GetUncompressedSource()) | |
| 235 elif format == 'compressed_bundle': | |
| 236 out_file.write(bundle.GetCompressedSource()) | |
| 237 out_file.write('\n') | |
| 238 | |
| 239 | |
| 240 def CreateOptionParser(): | |
| 241 parser = optparse.OptionParser(description=__doc__) | |
| 242 parser.usage = '%prog [options] <top_level_file>...' | |
| 243 parser.add_option('-d', '--dest_dir', action='store', metavar='DIR', | |
| 244 help=('Destination directory. Used when translating ' + | |
| 245 'input paths to output paths and when copying ' | |
| 246 'files.')) | |
| 247 parser.add_option('-o', '--output_file', action='store', metavar='FILE', | |
| 248 help=('File to output result to for modes that output ' | |
| 249 'a single file.')) | |
| 250 parser.add_option('-r', '--root', dest='roots', action='append', default=[], | |
| 251 metavar='ROOT', | |
| 252 help='Roots of directory trees to scan for sources.') | |
| 253 parser.add_option('-w', '--rewrite_prefix', action='append', default=[], | |
| 254 dest='prefix_map', metavar='SPEC', | |
| 255 help=('Two path prefixes, separated by colons ' + | |
| 256 'specifying that a file whose (relative) path ' + | |
| 257 'name starts with the first prefix should have ' + | |
| 258 'that prefix replaced by the second prefix to ' + | |
| 259 'form a path relative to the output directory.')) | |
| 260 parser.add_option('-m', '--mode', type='choice', action='store', | |
| 261 choices=['list', 'html', 'bundle', | |
| 262 'compressed_bundle', 'copy'], | |
| 263 default='list', metavar='MODE', | |
| 264 help=("Otput mode. One of 'list', 'html', 'bundle', " + | |
| 265 "'compressed_bundle' or 'copy'.")) | |
| 266 return parser | |
| 267 | |
| 268 | |
| 269 def main(): | |
| 270 options, args = CreateOptionParser().parse_args() | |
| 271 if len(args) < 1: | |
| 272 Die('At least one top-level source file must be specified.') | |
| 273 sources = ReadSources(options, args) | |
| 274 bundle = Bundle() | |
| 275 if len(options.roots) > 0: | |
| 276 CalcDeps(bundle, sources, args) | |
| 277 bundle.Add((sources[name] for name in args)) | |
| 278 if options.mode == 'copy': | |
| 279 if options.dest_dir is None: | |
| 280 Die('Must specify --dest_dir when copying.') | |
| 281 CopyFiles(bundle.GetSources(), options.dest_dir) | |
| 282 else: | |
| 283 if options.output_file: | |
| 284 out_file = open(options.output_file, 'w') | |
| 285 else: | |
| 286 out_file = sys.stdout | |
| 287 try: | |
| 288 WriteOutput(bundle, options.mode, out_file, options.dest_dir) | |
| 289 finally: | |
| 290 if options.output_file: | |
| 291 out_file.close() | |
| 292 | |
| 293 | |
| 294 if __name__ == '__main__': | |
| 295 main() | |
| OLD | NEW |