Home | History | Annotate | Download | only in build
      1 #!/usr/bin/python
      2 # Copyright (c) 2009 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 
      6 """Docbuilder for extension docs."""
      7 
      8 import os
      9 import os.path
     10 import shutil
     11 import sys
     12 import time
     13 import urllib
     14 
     15 from subprocess import Popen, PIPE
     16 from optparse import OptionParser
     17 
     18 _script_path = os.path.realpath(__file__)
     19 _build_dir = os.path.dirname(_script_path)
     20 _base_dir = os.path.normpath(_build_dir + "/..")
     21 _webkit_dir = _base_dir + "/../../../../third_party/WebKit"
     22 _devtools_dir = _webkit_dir + "/Source/WebCore/inspector/front-end"
     23 _static_dir = _base_dir + "/static"
     24 _js_dir = _base_dir + "/js"
     25 _template_dir = _base_dir + "/template"
     26 _samples_dir = _base_dir + "/examples"
     27 _extension_api_dir = os.path.normpath(_base_dir + "/../api")
     28 
     29 _extension_api_json = _extension_api_dir + "/extension_api.json"
     30 _devtools_api_json = _devtools_dir + "/ExtensionAPISchema.json"
     31 _api_template_html = _template_dir + "/api_template.html"
     32 _page_shell_html = _template_dir + "/page_shell.html"
     33 _generator_html = _build_dir + "/generator.html"
     34 _samples_json = _base_dir + "/samples.json"
     35 
     36 _expected_output_preamble = "#BEGIN"
     37 _expected_output_postamble = "#END"
     38 
     39 # HACK! This is required because we can only depend on python 2.4 and
     40 # the calling environment may not be setup to set the PYTHONPATH
     41 sys.path.append(os.path.normpath(_base_dir +
     42                                    "/../../../../third_party"))
     43 import simplejson as json
     44 from directory import Sample
     45 from directory import ApiManifest
     46 from directory import SamplesManifest
     47 
     48 def RenderPages(names, dump_render_tree):
     49   """
     50   Calls DumpRenderTree .../generator.html?<names> and writes the
     51   results to .../docs/<name>.html
     52   """
     53   if not names:
     54     raise Exception("RenderPage called with empty names param")
     55 
     56   generator_url = "file:" + urllib.pathname2url(_generator_html)
     57   generator_url += "?" + ",".join(names)
     58 
     59   # Start with a fresh copy of page shell for each file.
     60   # Save the current contents so that we can look for changes later.
     61   originals = {}
     62   for name in names:
     63     input_file = _base_dir + "/" + name + ".html"
     64 
     65     if (os.path.isfile(input_file)):
     66       originals[name] = open(input_file, 'rb').read()
     67       os.remove(input_file)
     68     else:
     69       originals[name] = ""
     70 
     71     shutil.copy(_page_shell_html, input_file)
     72 
     73   # Run DumpRenderTree and capture result
     74   dump_render_tree_timeout = 1000 * 60 * 5  # five minutes
     75   p = Popen(
     76       [dump_render_tree, "--test-shell",
     77        "%s %s" % (generator_url, dump_render_tree_timeout)],
     78       stdout=PIPE)
     79 
     80   # The remaining output will be the content of the generated pages.
     81   output = p.stdout.read()
     82 
     83   # Parse out just the JSON part.
     84   begin = output.find(_expected_output_preamble)
     85   end = output.rfind(_expected_output_postamble)
     86 
     87   if (begin < 0 or end < 0):
     88     raise Exception("%s returned invalid output:\n\n%s" %
     89         (dump_render_tree, output))
     90 
     91   begin += len(_expected_output_preamble)
     92 
     93   try:
     94     output_parsed = json.loads(output[begin:end])
     95   except ValueError, msg:
     96    raise Exception("Could not parse DumpRenderTree output as JSON. Error: " +
     97                    msg + "\n\nOutput was:\n" + output)
     98 
     99   changed_files = []
    100   for name in names:
    101     result = output_parsed[name].encode("utf8") + '\n'
    102 
    103     # Remove CRs that are appearing from captured DumpRenderTree output.
    104     result = result.replace('\r', '')
    105 
    106     # Remove page_shell
    107     input_file = _base_dir + "/" + name + ".html"
    108     os.remove(input_file)
    109 
    110     # Write output
    111     open(input_file, 'wb').write(result)
    112     if (originals[name] and result != originals[name]):
    113       changed_files.append(input_file)
    114 
    115   return changed_files
    116 
    117 
    118 def FindDumpRenderTree():
    119   # This is hacky. It is used to guess the location of the DumpRenderTree
    120   chrome_dir = os.path.normpath(_base_dir + "/../../../")
    121   src_dir = os.path.normpath(chrome_dir + "/../")
    122 
    123   search_locations = []
    124 
    125   if (sys.platform in ('cygwin', 'win32')):
    126     home_dir = os.path.normpath(os.getenv("HOMEDRIVE") + os.getenv("HOMEPATH"))
    127     search_locations.append(chrome_dir + "/Release/DumpRenderTree.exe")
    128     search_locations.append(chrome_dir + "/Debug/DumpRenderTree.exe")
    129     search_locations.append(home_dir + "/bin/DumpRenderTree/"
    130                             "DumpRenderTree.exe")
    131 
    132   if (sys.platform in ('linux', 'linux2')):
    133     search_locations.append(src_dir + "/sconsbuild/Release/DumpRenderTree")
    134     search_locations.append(src_dir + "/out/Release/DumpRenderTree")
    135     search_locations.append(src_dir + "/sconsbuild/Debug/DumpRenderTree")
    136     search_locations.append(src_dir + "/out/Debug/DumpRenderTree")
    137     search_locations.append(os.getenv("HOME") + "/bin/DumpRenderTree/"
    138                             "DumpRenderTree")
    139 
    140   if (sys.platform == 'darwin'):
    141     search_locations.append(src_dir +
    142         "/xcodebuild/Release/DumpRenderTree.app/Contents/MacOS/DumpRenderTree")
    143     search_locations.append(src_dir +
    144         "/xcodebuild/Debug/DumpRenderTree.app/Contents/MacOS/DumpRenderTree")
    145     search_locations.append(os.getenv("HOME") + "/bin/DumpRenderTree/" +
    146                             "DumpRenderTree.app/Contents/MacOS/DumpRenderTree")
    147 
    148   for loc in search_locations:
    149     if os.path.isfile(loc):
    150       return loc
    151 
    152   raise Exception("Could not find DumpRenderTree executable\n"
    153                   "**DumpRenderTree may need to be built**\n"
    154                   "Searched: \n" + "\n".join(search_locations) + "\n"
    155                   "To specify a path to DumpRenderTree use "
    156                   "--dump-render-tree-path")
    157 
    158 def GetStaticFileNames():
    159   static_files = os.listdir(_static_dir)
    160   return set(os.path.splitext(file_name)[0]
    161              for file_name in static_files
    162              if file_name.endswith(".html") and not file_name.startswith("."))
    163 
    164 def main():
    165   # Prevent windows from using cygwin python.
    166   if (sys.platform == "cygwin"):
    167     sys.exit("Building docs not supported for cygwin python. Please run the "
    168              "build.sh script instead, which uses depot_tools python.")
    169 
    170   parser = OptionParser()
    171   parser.add_option("--dump-render-tree-path", dest="dump_render_tree_path",
    172                     metavar="PATH",
    173                     help="path to DumpRenderTree executable")
    174   parser.add_option("--page-name", dest="page_name", metavar="PAGE",
    175                     help="only generate docs for PAGE.html")
    176   parser.add_option("--nozip", dest="zips", action="store_false",
    177                     help="do not generate zip files for samples",
    178                     default=True)
    179   options, args = parser.parse_args()
    180 
    181   if (options.dump_render_tree_path and
    182       os.path.isfile(options.dump_render_tree_path)):
    183     dump_render_tree = options.dump_render_tree_path
    184   else:
    185     dump_render_tree = FindDumpRenderTree()
    186 
    187   # Load the manifest of existing API Methods
    188   api_manifest = ApiManifest(_extension_api_json)
    189 
    190   # DevTools API is maintained separately, in WebCore land
    191   devtools_api_manifest = ApiManifest(_devtools_api_json)
    192 
    193   # Read static file names
    194   static_names = GetStaticFileNames()
    195 
    196   # Read module names
    197   module_names = (api_manifest.getModuleNames() |
    198       devtools_api_manifest.getModuleNames())
    199 
    200   # All pages to generate
    201   page_names = static_names | module_names
    202 
    203   # Allow the user to render a single page if they want
    204   if options.page_name:
    205     if options.page_name in page_names:
    206       page_names = [options.page_name]
    207     else:
    208       raise Exception("--page-name argument must be one of %s." %
    209                       ', '.join(sorted(page_names)))
    210 
    211   # Render a manifest file containing metadata about all the extension samples
    212   samples_manifest = SamplesManifest(_samples_dir, _base_dir, api_manifest)
    213   samples_manifest.writeToFile(_samples_json)
    214 
    215   # Write zipped versions of the samples listed in the manifest to the
    216   # filesystem, unless the user has disabled it
    217   if options.zips:
    218     modified_zips = samples_manifest.writeZippedSamples()
    219   else:
    220     modified_zips = []
    221 
    222   modified_files = RenderPages(page_names, dump_render_tree)
    223   modified_files.extend(modified_zips)
    224 
    225   if len(modified_files) == 0:
    226     print "Output files match existing files. No changes made."
    227   else:
    228     print ("ATTENTION: EXTENSION DOCS HAVE CHANGED\n" +
    229            "The following files have been modified and should be checked\n" +
    230            "into source control (ideally in the same changelist as the\n" +
    231            "underlying files that resulting in their changing).")
    232     for f in modified_files:
    233       print " * %s" % f
    234 
    235   # Hack. Sleep here, otherwise windows doesn't properly close the debug.log
    236   # and the os.remove will fail with a "Permission denied".
    237   time.sleep(1)
    238   debug_log = os.path.normpath(_build_dir + "/" + "debug.log")
    239   if (os.path.isfile(debug_log)):
    240     os.remove(debug_log)
    241 
    242   if 'EX_OK' in dir(os):
    243     return os.EX_OK
    244   else:
    245     return 0
    246 
    247 if __name__ == '__main__':
    248   sys.exit(main())
    249