• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2001 Google Inc. All Rights Reserved.
4#
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
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
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.
16
17"""Simple web server for browsing dependency graph data.
18
19This script is inlined into the final executable and spawned by
20it when needed.
21"""
22
23from __future__ import print_function
24
25try:
26    import http.server as httpserver
27    import socketserver
28except ImportError:
29    import BaseHTTPServer as httpserver
30    import SocketServer as socketserver
31import argparse
32import os
33import socket
34import subprocess
35import sys
36import webbrowser
37if sys.version_info >= (3, 2):
38    from html import escape
39else:
40    from cgi import escape
41try:
42    from urllib.request import unquote
43except ImportError:
44    from urllib2 import unquote
45from collections import namedtuple
46
47Node = namedtuple('Node', ['inputs', 'rule', 'target', 'outputs'])
48
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.
52#
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.
58#
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.
61
62def match_strip(line, prefix):
63    if not line.startswith(prefix):
64        return (False, line)
65    return (True, line[len(prefix):])
66
67def html_escape(text):
68    return escape(text, quote=True)
69
70def parse(text):
71    lines = iter(text.split('\n'))
72
73    target = None
74    rule = None
75    inputs = []
76    outputs = []
77
78    try:
79        target = next(lines)[:-1]  # strip trailing colon
80
81        line = next(lines)
82        (match, rule) = match_strip(line, '  input: ')
83        if match:
84            (match, line) = match_strip(next(lines), '    ')
85            while match:
86                type = None
87                (match, line) = match_strip(line, '| ')
88                if match:
89                    type = 'implicit'
90                (match, line) = match_strip(line, '|| ')
91                if match:
92                    type = 'order-only'
93                inputs.append((line, type))
94                (match, line) = match_strip(next(lines), '    ')
95
96        match, _ = match_strip(line, '  outputs:')
97        if match:
98            (match, line) = match_strip(next(lines), '    ')
99            while match:
100                outputs.append(line)
101                (match, line) = match_strip(next(lines), '    ')
102    except StopIteration:
103        pass
104
105    return Node(inputs, rule, target, outputs)
106
107def create_page(body):
108    return '''<!DOCTYPE html>
109<style>
110body {
111    font-family: sans;
112    font-size: 0.8em;
113    margin: 4ex;
114}
115h1 {
116    font-weight: normal;
117    font-size: 140%;
118    text-align: center;
119    margin: 0;
120}
121h2 {
122    font-weight: normal;
123    font-size: 120%;
124}
125tt {
126    font-family: WebKitHack, monospace;
127    white-space: nowrap;
128}
129.filelist {
130  -webkit-columns: auto 2;
131}
132</style>
133''' + body
134
135def generate_html(node):
136    document = ['<h1><tt>%s</tt></h1>' % html_escape(node.target)]
137
138    if node.inputs:
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):
144                extra = ''
145                if type:
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>')
150
151    if node.outputs:
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>')
158
159    return '\n'.join(document)
160
161def 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,)
166
167class RequestHandler(httpserver.BaseHTTPRequestHandler):
168    def do_GET(self):
169        assert self.path[0] == '/'
170        target = unquote(self.path[1:])
171
172        if target == '':
173            self.send_response(302)
174            self.send_header('Location', '?' + args.initial_target)
175            self.end_headers()
176            return
177
178        if not target.startswith('?'):
179            self.send_response(404)
180            self.end_headers()
181            return
182        target = target[1:]
183
184        ninja_output, ninja_error, exit_code = ninja_dump(target)
185        if exit_code == 0:
186            page_body = generate_html(parse(ninja_output.strip()))
187        else:
188            # Relay ninja's error message.
189            page_body = '<h1><tt>%s</tt></h1>' % html_escape(ninja_error)
190
191        self.send_response(200)
192        self.end_headers()
193        self.wfile.write(create_page(page_body).encode('utf-8'))
194
195    def log_message(self, format, *args):
196        pass  # Swallow console spam.
197
198parser = argparse.ArgumentParser(prog='ninja -t browse')
199parser.add_argument('--port', '-p', default=8000, type=int,
200    help='Port number to use (default %(default)d)')
201parser.add_argument('--hostname', '-a', default='localhost', type=str,
202    help='Hostname to bind to (default %(default)s)')
203parser.add_argument('--no-browser', action='store_true',
204    help='Do not open a webbrowser on startup.')
205
206parser.add_argument('--ninja-command', default='ninja',
207    help='Path to ninja binary (default %(default)s)')
208parser.add_argument('-f', default='build.ninja',
209    help='Path to build.ninja file (default %(default)s)')
210parser.add_argument('initial_target', default='all', nargs='?',
211    help='Initial target to show (default %(default)s)')
212
213class HTTPServer(socketserver.ThreadingMixIn, httpserver.HTTPServer):
214    # terminate server immediately when Python exits.
215    daemon_threads = True
216
217args = parser.parse_args()
218port = args.port
219hostname = args.hostname
220httpd = HTTPServer((hostname,port), RequestHandler)
221try:
222    if hostname == "":
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()
229except KeyboardInterrupt:
230    print()
231    pass  # Swallow console spam.
232
233
234