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