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