Home | History | Annotate | Download | only in build
      1 # Copyright (c) 2012 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 import sys
      5 import os
      6 import re
      7 
      8 class DepsException(Exception):
      9   pass
     10 
     11 """
     12 The core of this script is the calc_load_sequence function. In total, this
     13 walks over the provided javascript files and figures out their dependencies
     14 using the module definitions provided in each file. This allows us to, for
     15 example, have a trio of modules:
     16 
     17 foo.js:
     18    base.require('bar');
     19 and bar.js:
     20    base.require('baz');
     21 
     22 calc_load_sequence(['foo'], '.') will yield:
     23    [Module('baz'), Module('bar'), Module('foo')]
     24 
     25 which is, based on the dependencies, the correct sequence in which to load
     26 those modules.
     27 """
     28 
     29 class ResourceFinder(object):
     30   """Helper code for finding a module given a name and current module.
     31 
     32   The dependency resolution code in Module.resolve will find bits of code in the
     33   actual javascript that says things require('bar'). This
     34   code is responsible for figuring out what filename corresponds to 'bar' given
     35   a Module('foo').
     36   """
     37   def __init__(self, root_dir):
     38     self._root_dir = root_dir
     39     pass
     40 
     41   @property
     42   def root_dir(self):
     43     return self._root_dir
     44 
     45   def _find_and_load_filename(self, absolute_path):
     46     if not os.path.exists(absolute_path):
     47       return None, None
     48 
     49     f = open(absolute_path, 'r')
     50     contents = f.read()
     51     f.close()
     52 
     53     return absolute_path, contents
     54 
     55   def _find_and_load(self, current_module, requested_name, extension):
     56     assert current_module.filename
     57     pathy_name = requested_name.replace(".", os.sep)
     58     filename = pathy_name + extension
     59     absolute_path = os.path.join(self._root_dir, filename)
     60     return self._find_and_load_filename(absolute_path)
     61 
     62   def find_and_load_module(self, current_module, requested_module_name):
     63     return self._find_and_load(current_module, requested_module_name, ".js")
     64 
     65   def find_and_load_raw_script(self, current_module, filename):
     66     absolute_path = os.path.join(self._root_dir, filename)
     67     return self._find_and_load_filename(absolute_path)
     68 
     69   def find_and_load_style_sheet(self,
     70                                 current_module, requested_style_sheet_name):
     71     return self._find_and_load(
     72       current_module, requested_style_sheet_name, ".css")
     73 
     74 
     75 class StyleSheet(object):
     76   """Represents a stylesheet resource referenced by a module via the
     77   base.requireStylesheet(xxx) directive."""
     78   def __init__(self, name, filename, contents):
     79     self.name = name
     80     self.filename = filename
     81     self.contents = contents
     82 
     83   def __repr__(self):
     84     return "StyleSheet(%s)" % self.name
     85 
     86 class RawScript(object):
     87   """Represents a raw script resource referenced by a module via the
     88   base.requireRawScript(xxx) directive."""
     89   def __init__(self, name, filename, contents):
     90     self.name = name
     91     self.filename = filename
     92     self.contents = contents
     93 
     94   def __repr__(self):
     95     return "RawScript(%s)" % self.name
     96 
     97 def _tokenize_js(text):
     98   rest = text
     99   tokens = ["//", "/*", "*/", "\n"]
    100   while len(rest):
    101     indices = [rest.find(token) for token in tokens]
    102     found_indices = [index for index in indices if index >= 0]
    103 
    104     if len(found_indices) == 0:
    105       # end of string
    106       yield rest
    107       return
    108 
    109     min_index = min(found_indices)
    110     token_with_min = tokens[indices.index(min_index)]
    111 
    112     if min_index > 0:
    113       yield rest[:min_index]
    114 
    115     yield rest[min_index:min_index + len(token_with_min)]
    116     rest = rest[min_index + len(token_with_min):]
    117 
    118 def _strip_js_comments(text):
    119   result_tokens = []
    120   token_stream = _tokenize_js(text).__iter__()
    121   while True:
    122     try:
    123       t = token_stream.next()
    124     except StopIteration:
    125       break
    126 
    127     if t == "//":
    128       while True:
    129         try:
    130           t2 = token_stream.next()
    131           if t2 == "\n":
    132             break
    133         except StopIteration:
    134           break
    135     elif t == '/*':
    136       nesting = 1
    137       while True:
    138         try:
    139           t2 = token_stream.next()
    140           if t2 == "/*":
    141             nesting += 1
    142           elif t2 == "*/":
    143             nesting -= 1
    144             if nesting == 0:
    145               break
    146         except StopIteration:
    147           break
    148     else:
    149       result_tokens.append(t)
    150   return "".join(result_tokens)
    151 
    152 def _MangleRawScriptFilenameToModuleName(filename):
    153   name = filename
    154   name = name.replace(os.sep, ':')
    155   name = name.replace('..', '!!')
    156   return name
    157 
    158 class Module(object):
    159   """Represents a javascript module. It can either be directly requested, e.g.
    160   passed in by name to calc_load_sequence, or created by being referenced a
    161   module via the base.require(xxx) directive.
    162 
    163   Interesting properties on this object are:
    164 
    165   - filename: the file of the actual module
    166   - contents: the actual text contents of the module
    167   - style_sheets: StyleSheet objects that this module relies on for styling
    168     information.
    169   - dependent_modules: other modules that this module needs in order to run
    170   """
    171   def __init__(self, name = None):
    172     self.name = name
    173     self.filename = None
    174     self.contents = None
    175 
    176     self.dependent_module_names = []
    177     self.dependent_modules = []
    178     self.dependent_raw_script_names = []
    179     self.dependent_raw_scripts = []
    180     self.style_sheet_names = []
    181     self.style_sheets = []
    182 
    183   def __repr__(self):
    184     return "Module(%s)" % self.name
    185 
    186   def load_and_parse(self, module_filename,
    187                      module_contents = None,
    188                      decl_required = True):
    189     if not module_contents:
    190       f = open(module_filename, 'r')
    191       self.contents = f.read()
    192       f.close()
    193     else:
    194       self.contents = module_contents
    195     self.filename = module_filename
    196     self.parse_definition_(self.contents, decl_required)
    197 
    198   def resolve(self, all_resources, resource_finder):
    199     if "scripts" not in all_resources:
    200       all_resources["scripts"] = {}
    201     if "style_sheets" not in all_resources:
    202       all_resources["style_sheets"] = {}
    203     if "raw_scripts" not in all_resources:
    204       all_resources["raw_scripts"] = {}
    205 
    206     assert self.filename
    207 
    208     for name in self.dependent_module_names:
    209       if name in all_resources["scripts"]:
    210         assert all_resources["scripts"][name].contents
    211         self.dependent_modules.append(all_resources["scripts"][name])
    212         continue
    213 
    214       filename, contents = resource_finder.find_and_load_module(self, name)
    215       if not filename:
    216         raise DepsException("Could not find a file for module %s" % name)
    217 
    218       module = Module(name)
    219       all_resources["scripts"][name] = module
    220       self.dependent_modules.append(module)
    221       module.load_and_parse(filename, contents)
    222       module.resolve(all_resources, resource_finder)
    223 
    224     for name in self.dependent_raw_script_names:
    225       filename, contents = resource_finder.find_and_load_raw_script(self, name)
    226       if not filename:
    227         raise DepsException("Could not find a file for module %s" % name)
    228 
    229       if name in all_resources["raw_scripts"]:
    230         assert all_resources["raw_scripts"][name].contents
    231         self.dependent_raw_scripts.append(all_resources["raw_scripts"][name])
    232         continue
    233 
    234       raw_script = RawScript(name, filename, contents)
    235       all_resources["raw_scripts"][name] = raw_script
    236       self.dependent_raw_scripts.append(raw_script)
    237 
    238     for name in self.style_sheet_names:
    239       if name in all_resources["style_sheets"]:
    240         assert all_resources["style_sheets"][name].contents
    241         self.style_sheets.append(all_resources["scripts"][name])
    242         continue
    243 
    244       filename, contents = resource_finder.find_and_load_style_sheet(self, name)
    245       if not filename:
    246         raise DepsException("Could not find a file for stylesheet %s" % name)
    247 
    248       style_sheet = StyleSheet(name, filename, contents)
    249       all_resources["style_sheets"][name] = style_sheet
    250       self.style_sheets.append(style_sheet)
    251 
    252   def compute_load_sequence_recursive(self, load_sequence, already_loaded_set):
    253     for dependent_module in self.dependent_modules:
    254       dependent_module.compute_load_sequence_recursive(load_sequence,
    255                                                        already_loaded_set)
    256     if self.name not in already_loaded_set:
    257       already_loaded_set.add(self.name)
    258       load_sequence.append(self)
    259 
    260   def parse_definition_(self, text, decl_required = True):
    261     if not decl_required and not self.name:
    262       raise Exception("Module.name must be set for decl_required to be false.")
    263 
    264     stripped_text = _strip_js_comments(text)
    265     rest = stripped_text
    266     while True:
    267       # Things to search for.
    268       m_r = re.search("""base\s*\.\s*require\((["'])(.+?)\\1\)""",
    269                       rest, re.DOTALL)
    270       m_s = re.search("""base\s*\.\s*requireStylesheet\((["'])(.+?)\\1\)""",
    271                       rest, re.DOTALL)
    272       m_irs = re.search("""base\s*\.\s*requireRawScript\((["'])(.+?)\\1\)""",
    273                       rest, re.DOTALL)
    274       matches = [m for m in [m_r, m_s, m_irs] if m]
    275 
    276       # Figure out which was first.
    277       matches.sort(key=lambda x: x.start())
    278       if len(matches):
    279         m = matches[0]
    280       else:
    281         break
    282 
    283       if m == m_r:
    284         dependent_module_name = m.group(2)
    285         if '/' in dependent_module_name:
    286           raise DepsException("Slashes are not allowed in module names. "
    287                               "Use '.' instead: %s" % dependent_module_name)
    288         if dependent_module_name.endswith('js'):
    289           raise DepsException("module names shouldn't end with .js"
    290                               "The module system will append that for you: %s" %
    291                               dependent_module_name)
    292         self.dependent_module_names.append(dependent_module_name)
    293       elif m == m_s:
    294         style_sheet_name = m.group(2)
    295         if '/' in style_sheet_name:
    296           raise DepsException("Slashes are not allowed in style sheet names. "
    297                               "Use '.' instead: %s" % style_sheet_name)
    298         if style_sheet_name.endswith('.css'):
    299           raise DepsException("Style sheets should not end in .css. "
    300                               "The module system will append that for you" %
    301                               style_sheet_name)
    302         self.style_sheet_names.append(style_sheet_name)
    303       elif m == m_irs:
    304         name = m.group(2)
    305         self.dependent_raw_script_names.append(name)
    306 
    307       rest = rest[m.end():]
    308 
    309 
    310 def calc_load_sequence(filenames, toplevel_dir):
    311   """Given a list of starting javascript files, figure out all the Module
    312   objects that need to be loaded to satisfiy their dependencies.
    313 
    314   The javascript files shoud specify their dependencies in a format that is
    315   textually equivalent to base.js' require syntax, namely:
    316 
    317      base.require(module1);
    318      base.require(module2);
    319      base.requireStylesheet(stylesheet);
    320 
    321   The output of this function is an array of Module objects ordered by
    322   dependency.
    323   """
    324   all_resources = {}
    325   all_resources["scripts"] = {}
    326   toplevel_modules = []
    327   root_dir = ''
    328   if filenames:
    329     root_dir = os.path.abspath(os.path.dirname(filenames[0]))
    330   resource_finder = ResourceFinder(root_dir)
    331   for filename in filenames:
    332     if not os.path.exists(filename):
    333       raise Exception("Could not find %s" % filename)
    334 
    335     rel_filename = os.path.relpath(filename, toplevel_dir)
    336     dirname = os.path.dirname(rel_filename)
    337     modname  = os.path.splitext(os.path.basename(rel_filename))[0]
    338     if len(dirname):
    339       name = dirname.replace('/', '.') + '.' + modname
    340     else:
    341       name = modname
    342 
    343     if name in all_resources["scripts"]:
    344       continue
    345 
    346     module = Module(name)
    347     module.load_and_parse(filename, decl_required = False)
    348     all_resources["scripts"][module.name] = module
    349     module.resolve(all_resources, resource_finder)
    350 
    351   # Find the root modules: ones who have no dependencies.
    352   module_ref_counts = {}
    353   for module in all_resources["scripts"].values():
    354     module_ref_counts[module.name] = 0
    355 
    356   def inc_ref_count(name):
    357     module_ref_counts[name] = module_ref_counts[name] + 1
    358   for module in all_resources["scripts"].values():
    359     for dependent_module in module.dependent_modules:
    360       inc_ref_count(dependent_module.name)
    361 
    362   root_modules = [all_resources["scripts"][name]
    363                   for name, ref_count in module_ref_counts.items()
    364                   if ref_count == 0]
    365 
    366   root_modules.sort(lambda x, y: cmp(x.name, y.name))
    367 
    368   already_loaded_set = set()
    369   load_sequence = []
    370   for module in root_modules:
    371     module.compute_load_sequence_recursive(load_sequence, already_loaded_set)
    372   return load_sequence
    373