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
8import os
9import os.path
10import shutil
11import sys
12import time
13import urllib
14
15from subprocess import Popen, PIPE
16from 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
41sys.path.append(os.path.normpath(_base_dir +
42                                   "/../../../../third_party"))
43import simplejson as json
44from directory import Sample
45from directory import ApiManifest
46from directory import SamplesManifest
47
48def 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
118def 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
158def 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
164def 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
247if __name__ == '__main__':
248  sys.exit(main())
249