3 # Copyright 2001 Google Inc. All Rights Reserved.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Simple web server for browsing dependency graph data.
19 This script is inlined into the final executable and spawned by
23 from __future__ import print_function
26 import http.server as httpserver
29 import BaseHTTPServer as httpserver
30 import SocketServer as socketserver
37 if sys.version_info >= (3, 2):
38 from html import escape
40 from cgi import escape
42 from urllib.request import unquote
44 from urllib2 import unquote
45 from collections import namedtuple
47 Node = namedtuple('Node', ['inputs', 'rule', 'target', 'outputs'])
49 # Ideally we'd allow you to navigate to a build edge or a build node,
50 # with appropriate views for each. But there's no way to *name* a build
51 # edge so we can only display nodes.
53 # For a given node, it has at most one input edge, which has n
54 # different inputs. This becomes node.inputs. (We leave out the
55 # outputs of the input edge due to what follows.) The node can have
56 # multiple dependent output edges. Rather than attempting to display
57 # those, they are summarized by taking the union of all their outputs.
59 # This means there's no single view that shows you all inputs and outputs
60 # of an edge. But I think it's less confusing than alternatives.
62 def match_strip(line, prefix):
63 if not line.startswith(prefix):
65 return (True, line[len(prefix):])
67 def html_escape(text):
68 return escape(text, quote=True)
71 lines = iter(text.split('\n'))
79 target = next(lines)[:-1] # strip trailing colon
82 (match, rule) = match_strip(line, ' input: ')
84 (match, line) = match_strip(next(lines), ' ')
87 (match, line) = match_strip(line, '| ')
90 (match, line) = match_strip(line, '|| ')
93 inputs.append((line, type))
94 (match, line) = match_strip(next(lines), ' ')
96 match, _ = match_strip(line, ' outputs:')
98 (match, line) = match_strip(next(lines), ' ')
101 (match, line) = match_strip(next(lines), ' ')
102 except StopIteration:
105 return Node(inputs, rule, target, outputs)
107 def create_page(body):
108 return '''<!DOCTYPE html>
126 font-family: WebKitHack, monospace;
130 -webkit-columns: auto 2;
135 def generate_html(node):
136 document = ['<h1><tt>%s</tt></h1>' % html_escape(node.target)]
139 document.append('<h2>target is built using rule <tt>%s</tt> of</h2>' %
140 html_escape(node.rule))
141 if len(node.inputs) > 0:
142 document.append('<div class=filelist>')
143 for input, type in sorted(node.inputs):
146 extra = ' (%s)' % html_escape(type)
147 document.append('<tt><a href="?%s">%s</a>%s</tt><br>' %
148 (html_escape(input), html_escape(input), extra))
149 document.append('</div>')
152 document.append('<h2>dependent edges build:</h2>')
153 document.append('<div class=filelist>')
154 for output in sorted(node.outputs):
155 document.append('<tt><a href="?%s">%s</a></tt><br>' %
156 (html_escape(output), html_escape(output)))
157 document.append('</div>')
159 return '\n'.join(document)
161 def ninja_dump(target):
162 cmd = [args.ninja_command, '-f', args.f, '-t', 'query', target]
163 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
164 universal_newlines=True)
165 return proc.communicate() + (proc.returncode,)
167 class RequestHandler(httpserver.BaseHTTPRequestHandler):
169 assert self.path[0] == '/'
170 target = unquote(self.path[1:])
173 self.send_response(302)
174 self.send_header('Location', '?' + args.initial_target)
178 if not target.startswith('?'):
179 self.send_response(404)
184 ninja_output, ninja_error, exit_code = ninja_dump(target)
186 page_body = generate_html(parse(ninja_output.strip()))
188 # Relay ninja's error message.
189 page_body = '<h1><tt>%s</tt></h1>' % html_escape(ninja_error)
191 self.send_response(200)
193 self.wfile.write(create_page(page_body).encode('utf-8'))
195 def log_message(self, format, *args):
196 pass # Swallow console spam.
198 parser = argparse.ArgumentParser(prog='ninja -t browse')
199 parser.add_argument('--port', '-p', default=8000, type=int,
200 help='Port number to use (default %(default)d)')
201 parser.add_argument('--hostname', '-a', default='localhost', type=str,
202 help='Hostname to bind to (default %(default)s)')
203 parser.add_argument('--no-browser', action='store_true',
204 help='Do not open a webbrowser on startup.')
206 parser.add_argument('--ninja-command', default='ninja',
207 help='Path to ninja binary (default %(default)s)')
208 parser.add_argument('-f', default='build.ninja',
209 help='Path to build.ninja file (default %(default)s)')
210 parser.add_argument('initial_target', default='all', nargs='?',
211 help='Initial target to show (default %(default)s)')
213 class HTTPServer(socketserver.ThreadingMixIn, httpserver.HTTPServer):
214 # terminate server immediately when Python exits.
215 daemon_threads = True
217 args = parser.parse_args()
219 hostname = args.hostname
220 httpd = HTTPServer((hostname,port), RequestHandler)
223 hostname = socket.gethostname()
224 print('Web server running on %s:%d, ctl-C to abort...' % (hostname,port) )
225 print('Web server pid %d' % os.getpid(), file=sys.stderr )
226 if not args.no_browser:
227 webbrowser.open_new('http://%s:%s' % (hostname, port) )
228 httpd.serve_forever()
229 except KeyboardInterrupt:
231 pass # Swallow console spam.