builder.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. #!/usr/bin/python
  2. # Copyright 2014 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Closure builder for Javascript."""
  6. import argparse
  7. import os
  8. import re
  9. import shlex
  10. _BASE_REGEX_STRING = r'^\s*goog\.%s\(\s*[\'"](.+)[\'"]\s*\)'
  11. require_regex = re.compile(_BASE_REGEX_STRING % 'require')
  12. provide_regex = re.compile(_BASE_REGEX_STRING % 'provide')
  13. base = os.path.join('third_party',
  14. 'closure_library',
  15. 'closure',
  16. 'goog',
  17. 'base.js')
  18. def process_file(filename):
  19. """Extracts provided and required namespaces.
  20. Description:
  21. Scans Javascript file for provided and required namespaces.
  22. Args:
  23. filename: name of the file to process.
  24. Returns:
  25. Pair of lists, where the first list contains namespaces provided by the file
  26. and the second contains a list of requirements.
  27. """
  28. provides = []
  29. requires = []
  30. with open(filename, 'r') as file_handle:
  31. for line in file_handle:
  32. if re.match(require_regex, line):
  33. requires.append(re.search(require_regex, line).group(1))
  34. if re.match(provide_regex, line):
  35. provides.append(re.search(provide_regex, line).group(1))
  36. return provides, requires
  37. def extract_dependencies(filename, providers, requirements):
  38. """Extracts provided and required namespaces for a file.
  39. Description:
  40. Updates maps for namespace providers and file prerequisites.
  41. Args:
  42. filename: Path of the file to process.
  43. providers: Mapping of namespace to filename that provides the namespace.
  44. requirements: Mapping of filename to a list of prerequisite namespaces.
  45. """
  46. p, r = process_file(filename)
  47. for name in p:
  48. providers[name] = filename
  49. for name in r:
  50. if filename not in requirements:
  51. requirements[filename] = []
  52. requirements[filename].append(name)
  53. def export(target_file, source_filename, providers, requirements, processed):
  54. """Writes the contents of a file.
  55. Description:
  56. Appends the contents of the source file to the target file. In order to
  57. preserve proper dependencies, each file has its required namespaces
  58. processed before exporting the source file itself. The set of exported files
  59. is tracked to guard against multiple exports of the same file. Comments as
  60. well as 'provide' and 'require' statements are removed during to export to
  61. reduce file size.
  62. Args:
  63. target_file: Handle to target file for export.
  64. source_filename: Name of the file to export.
  65. providers: Map of namespace to filename.
  66. requirements: Map of filename to required namespaces.
  67. processed: Set of processed files.
  68. Returns:
  69. """
  70. # Filename may have already been processed if it was a requirement of a
  71. # previous exported file.
  72. if source_filename in processed:
  73. return
  74. # Export requirements before file.
  75. if source_filename in requirements:
  76. for namespace in requirements[source_filename]:
  77. if namespace in providers:
  78. dependency = providers[namespace]
  79. if dependency:
  80. export(target_file, dependency, providers, requirements, processed)
  81. processed.add(source_filename)
  82. # Export file
  83. for name in providers:
  84. if providers[name] == source_filename:
  85. target_file.write('// %s%s' % (name, os.linesep))
  86. source_file = open(source_filename, 'r')
  87. try:
  88. comment_block = False
  89. for line in source_file:
  90. # Skip require statements.
  91. if not re.match(require_regex, line):
  92. formatted = line.rstrip()
  93. if comment_block:
  94. # Scan for trailing */ in multi-line comment.
  95. index = formatted.find('*/')
  96. if index >= 0:
  97. formatted = formatted[index + 2:]
  98. comment_block = False
  99. else:
  100. formatted = ''
  101. # Remove full-line // style comments.
  102. if formatted.lstrip().startswith('//'):
  103. formatted = ''
  104. # Remove /* */ style comments.
  105. start_comment = formatted.find('/*')
  106. end_comment = formatted.find('*/')
  107. while start_comment >= 0:
  108. if end_comment > start_comment:
  109. formatted = (formatted[:start_comment]
  110. + formatted[end_comment + 2:])
  111. start_comment = formatted.find('/*')
  112. end_comment = formatted.find('*/')
  113. else:
  114. formatted = formatted[:start_comment]
  115. comment_block = True
  116. start_comment = -1
  117. if formatted.strip():
  118. target_file.write('%s%s' % (formatted, os.linesep))
  119. finally:
  120. source_file.close()
  121. target_file.write('\n')
  122. def extract_sources(options):
  123. """Extracts list of sources based on command line options.
  124. Args:
  125. options: Parsed command line options.
  126. Returns:
  127. List of source files. If the path option is specified then file paths are
  128. absolute. Otherwise, relative paths may be used.
  129. """
  130. sources = []
  131. # Optionally load list of source files from a json file. Useful when the
  132. # list of files to process is too long for the command line.
  133. with open(options.sources_list[0], 'r') as f:
  134. sources = shlex.split(f.read())
  135. if options.path:
  136. sources = [os.path.join(options.path, source) for source in sources]
  137. return sources
  138. def main():
  139. """The entrypoint for this script."""
  140. parser = argparse.ArgumentParser()
  141. parser.add_argument('--sources-list', nargs=1)
  142. parser.add_argument('--target', nargs=1)
  143. parser.add_argument('--path', nargs='?')
  144. options = parser.parse_args()
  145. sources = extract_sources(options)
  146. assert sources, 'Missing source files.'
  147. providers = {}
  148. requirements = {}
  149. for filename in sources:
  150. extract_dependencies(filename, providers, requirements)
  151. with open(options.target[0], 'w') as target_file:
  152. target_file.write('var CLOSURE_NO_DEPS=true;%s' % os.linesep)
  153. processed = set()
  154. base_path = base
  155. if options.path:
  156. base_path = os.path.join(options.path, base_path)
  157. export(target_file, base_path, providers, requirements, processed)
  158. for source_filename in sources:
  159. export(target_file, source_filename, providers, requirements, processed)
  160. if __name__ == '__main__':
  161. main()