blob: 653cbe91f6be6fec58193d7232483a60a0cafc4e [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2001 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Simple web server for browsing dependency graph data.
This script is inlined into the final executable and spawned by
it when needed.
"""
from __future__ import print_function
try:
import http.server as httpserver
import socketserver
except ImportError:
import BaseHTTPServer as httpserver
import SocketServer as socketserver
import argparse
import os
import socket
import subprocess
import sys
import webbrowser
if sys.version_info >= (3, 2):
from html import escape
else:
from cgi import escape
try:
from urllib.request import unquote
except ImportError:
from urllib2 import unquote
from collections import namedtuple
Node = namedtuple('Node', ['inputs', 'rule', 'target', 'outputs'])
# Ideally we'd allow you to navigate to a build edge or a build node,
# with appropriate views for each. But there's no way to *name* a build
# edge so we can only display nodes.
#
# For a given node, it has at most one input edge, which has n
# different inputs. This becomes node.inputs. (We leave out the
# outputs of the input edge due to what follows.) The node can have
# multiple dependent output edges. Rather than attempting to display
# those, they are summarized by taking the union of all their outputs.
#
# This means there's no single view that shows you all inputs and outputs
# of an edge. But I think it's less confusing than alternatives.
def match_strip(line, prefix):
if not line.startswith(prefix):
return (False, line)
return (True, line[len(prefix):])
def html_escape(text):
return escape(text, quote=True)
def parse(text):
lines = iter(text.split('\n'))
target = None
rule = None
inputs = []
outputs = []
try:
target = next(lines)[:-1] # strip trailing colon
line = next(lines)
(match, rule) = match_strip(line, ' input: ')
if match:
(match, line) = match_strip(next(lines), ' ')
while match:
type = None
(match, line) = match_strip(line, '| ')
if match:
type = 'implicit'
(match, line) = match_strip(line, '|| ')
if match:
type = 'order-only'
inputs.append((line, type))
(match, line) = match_strip(next(lines), ' ')
match, _ = match_strip(line, ' outputs:')
if match:
(match, line) = match_strip(next(lines), ' ')
while match:
outputs.append(line)
(match, line) = match_strip(next(lines), ' ')
except StopIteration:
pass
return Node(inputs, rule, target, outputs)
def create_page(body):
return '''<!DOCTYPE html>
<style>
body {
font-family: sans;
font-size: 0.8em;
margin: 4ex;
}
h1 {
font-weight: normal;
font-size: 140%;
text-align: center;
margin: 0;
}
h2 {
font-weight: normal;
font-size: 120%;
}
tt {
font-family: WebKitHack, monospace;
white-space: nowrap;
}
.filelist {
-webkit-columns: auto 2;
}
</style>
''' + body
def generate_html(node):
document = ['<h1><tt>%s</tt></h1>' % html_escape(node.target)]
if node.inputs:
document.append('<h2>target is built using rule <tt>%s</tt> of</h2>' %
html_escape(node.rule))
if len(node.inputs) > 0:
document.append('<div class=filelist>')
for input, type in sorted(node.inputs):
extra = ''
if type:
extra = ' (%s)' % html_escape(type)
document.append('<tt><a href="?%s">%s</a>%s</tt><br>' %
(html_escape(input), html_escape(input), extra))
document.append('</div>')
if node.outputs:
document.append('<h2>dependent edges build:</h2>')
document.append('<div class=filelist>')
for output in sorted(node.outputs):
document.append('<tt><a href="?%s">%s</a></tt><br>' %
(html_escape(output), html_escape(output)))
document.append('</div>')
return '\n'.join(document)
def ninja_dump(target):
cmd = [args.ninja_command, '-f', args.f, '-t', 'query', target]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True)
return proc.communicate() + (proc.returncode,)
class RequestHandler(httpserver.BaseHTTPRequestHandler):
def do_GET(self):
assert self.path[0] == '/'
target = unquote(self.path[1:])
if target == '':
self.send_response(302)
self.send_header('Location', '?' + args.initial_target)
self.end_headers()
return
if not target.startswith('?'):
self.send_response(404)
self.end_headers()
return
target = target[1:]
ninja_output, ninja_error, exit_code = ninja_dump(target)
if exit_code == 0:
page_body = generate_html(parse(ninja_output.strip()))
else:
# Relay ninja's error message.
page_body = '<h1><tt>%s</tt></h1>' % html_escape(ninja_error)
self.send_response(200)
self.end_headers()
self.wfile.write(create_page(page_body).encode('utf-8'))
def log_message(self, format, *args):
pass # Swallow console spam.
parser = argparse.ArgumentParser(prog='ninja -t browse')
parser.add_argument('--port', '-p', default=8000, type=int,
help='Port number to use (default %(default)d)')
parser.add_argument('--hostname', '-a', default='localhost', type=str,
help='Hostname to bind to (default %(default)s)')
parser.add_argument('--no-browser', action='store_true',
help='Do not open a webbrowser on startup.')
parser.add_argument('--ninja-command', default='ninja',
help='Path to ninja binary (default %(default)s)')
parser.add_argument('-f', default='build.ninja',
help='Path to build.ninja file (default %(default)s)')
parser.add_argument('initial_target', default='all', nargs='?',
help='Initial target to show (default %(default)s)')
class HTTPServer(socketserver.ThreadingMixIn, httpserver.HTTPServer):
# terminate server immediately when Python exits.
daemon_threads = True
args = parser.parse_args()
port = args.port
hostname = args.hostname
httpd = HTTPServer((hostname,port), RequestHandler)
try:
if hostname == "":
hostname = socket.gethostname()
print('Web server running on %s:%d, ctl-C to abort...' % (hostname,port) )
print('Web server pid %d' % os.getpid(), file=sys.stderr )
if not args.no_browser:
webbrowser.open_new('http://%s:%s' % (hostname, port) )
httpd.serve_forever()
except KeyboardInterrupt:
print()
pass # Swallow console spam.