1#!/usr/bin/env python3 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 23try: 24 import http.server as httpserver 25 import socketserver 26except ImportError: 27 import BaseHTTPServer as httpserver 28 import SocketServer as socketserver 29import argparse 30import os 31import socket 32import subprocess 33import sys 34import webbrowser 35if sys.version_info >= (3, 2): 36 from html import escape 37else: 38 from cgi import escape 39try: 40 from urllib.request import unquote 41except ImportError: 42 from urllib2 import unquote 43from collections import namedtuple 44 45Node = namedtuple('Node', ['inputs', 'rule', 'target', 'outputs']) 46 47# Ideally we'd allow you to navigate to a build edge or a build node, 48# with appropriate views for each. But there's no way to *name* a build 49# edge so we can only display nodes. 50# 51# For a given node, it has at most one input edge, which has n 52# different inputs. This becomes node.inputs. (We leave out the 53# outputs of the input edge due to what follows.) The node can have 54# multiple dependent output edges. Rather than attempting to display 55# those, they are summarized by taking the union of all their outputs. 56# 57# This means there's no single view that shows you all inputs and outputs 58# of an edge. But I think it's less confusing than alternatives. 59 60def match_strip(line, prefix): 61 if not line.startswith(prefix): 62 return (False, line) 63 return (True, line[len(prefix):]) 64 65def html_escape(text): 66 return escape(text, quote=True) 67 68def parse(text): 69 lines = iter(text.split('\n')) 70 71 target = None 72 rule = None 73 inputs = [] 74 outputs = [] 75 76 try: 77 target = next(lines)[:-1] # strip trailing colon 78 79 line = next(lines) 80 (match, rule) = match_strip(line, ' input: ') 81 if match: 82 (match, line) = match_strip(next(lines), ' ') 83 while match: 84 type = None 85 (match, line) = match_strip(line, '| ') 86 if match: 87 type = 'implicit' 88 (match, line) = match_strip(line, '|| ') 89 if match: 90 type = 'order-only' 91 inputs.append((line, type)) 92 (match, line) = match_strip(next(lines), ' ') 93 94 match, _ = match_strip(line, ' outputs:') 95 if match: 96 (match, line) = match_strip(next(lines), ' ') 97 while match: 98 outputs.append(line) 99 (match, line) = match_strip(next(lines), ' ') 100 except StopIteration: 101 pass 102 103 return Node(inputs, rule, target, outputs) 104 105def create_page(body): 106 return '''<!DOCTYPE html> 107<style> 108body { 109 font-family: sans; 110 font-size: 0.8em; 111 margin: 4ex; 112} 113h1 { 114 font-weight: normal; 115 font-size: 140%; 116 text-align: center; 117 margin: 0; 118} 119h2 { 120 font-weight: normal; 121 font-size: 120%; 122} 123tt { 124 font-family: WebKitHack, monospace; 125 white-space: nowrap; 126} 127.filelist { 128 -webkit-columns: auto 2; 129} 130</style> 131''' + body 132 133def generate_html(node): 134 document = ['<h1><tt>%s</tt></h1>' % html_escape(node.target)] 135 136 if node.inputs: 137 document.append('<h2>target is built using rule <tt>%s</tt> of</h2>' % 138 html_escape(node.rule)) 139 if len(node.inputs) > 0: 140 document.append('<div class=filelist>') 141 for input, type in sorted(node.inputs): 142 extra = '' 143 if type: 144 extra = ' (%s)' % html_escape(type) 145 document.append('<tt><a href="?%s">%s</a>%s</tt><br>' % 146 (html_escape(input), html_escape(input), extra)) 147 document.append('</div>') 148 149 if node.outputs: 150 document.append('<h2>dependent edges build:</h2>') 151 document.append('<div class=filelist>') 152 for output in sorted(node.outputs): 153 document.append('<tt><a href="?%s">%s</a></tt><br>' % 154 (html_escape(output), html_escape(output))) 155 document.append('</div>') 156 157 return '\n'.join(document) 158 159def ninja_dump(target): 160 cmd = [args.ninja_command, '-f', args.f, '-t', 'query', target] 161 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 162 universal_newlines=True) 163 return proc.communicate() + (proc.returncode,) 164 165class RequestHandler(httpserver.BaseHTTPRequestHandler): 166 def do_GET(self): 167 assert self.path[0] == '/' 168 target = unquote(self.path[1:]) 169 170 if target == '': 171 self.send_response(302) 172 self.send_header('Location', '?' + args.initial_target) 173 self.end_headers() 174 return 175 176 if not target.startswith('?'): 177 self.send_response(404) 178 self.end_headers() 179 return 180 target = target[1:] 181 182 ninja_output, ninja_error, exit_code = ninja_dump(target) 183 if exit_code == 0: 184 page_body = generate_html(parse(ninja_output.strip())) 185 else: 186 # Relay ninja's error message. 187 page_body = '<h1><tt>%s</tt></h1>' % html_escape(ninja_error) 188 189 self.send_response(200) 190 self.end_headers() 191 self.wfile.write(create_page(page_body).encode('utf-8')) 192 193 def log_message(self, format, *args): 194 pass # Swallow console spam. 195 196parser = argparse.ArgumentParser(prog='ninja -t browse') 197parser.add_argument('--port', '-p', default=8000, type=int, 198 help='Port number to use (default %(default)d)') 199parser.add_argument('--hostname', '-a', default='localhost', type=str, 200 help='Hostname to bind to (default %(default)s)') 201parser.add_argument('--no-browser', action='store_true', 202 help='Do not open a webbrowser on startup.') 203 204parser.add_argument('--ninja-command', default='ninja', 205 help='Path to ninja binary (default %(default)s)') 206parser.add_argument('-f', default='build.ninja', 207 help='Path to build.ninja file (default %(default)s)') 208parser.add_argument('initial_target', default='all', nargs='?', 209 help='Initial target to show (default %(default)s)') 210 211class HTTPServer(socketserver.ThreadingMixIn, httpserver.HTTPServer): 212 # terminate server immediately when Python exits. 213 daemon_threads = True 214 215args = parser.parse_args() 216port = args.port 217hostname = args.hostname 218httpd = HTTPServer((hostname,port), RequestHandler) 219try: 220 if hostname == "": 221 hostname = socket.gethostname() 222 print('Web server running on %s:%d, ctl-C to abort...' % (hostname,port) ) 223 print('Web server pid %d' % os.getpid(), file=sys.stderr ) 224 if not args.no_browser: 225 webbrowser.open_new('http://%s:%s' % (hostname, port) ) 226 httpd.serve_forever() 227except KeyboardInterrupt: 228 print() 229 pass # Swallow console spam. 230 231 232